Porting wrist-list to Fitbit OS5
wrist-list
Last year I released wrist-list on the Fitbit app store. Its gone quite well with 2,500 installs. Fitbit have continued to release new devices that run Fitbit OS and I have received requests to make wrist-list work on them. The new devices: Versa 3 and Sense run the new Fitbit OS5 and its only available on the new devices, and Fitbit OS4 is only available on the old devices.
So it was always going to be non-trivial to setup my project to support both OS4 and OS5, even though there was a migration guide.
Getting build and run to work
The guides detailed a number of breaking changes and required changes to the project structure. As the Q&A section makes clear its not possible to target both OS4 and OS5 from the same project. I could have two separate projects and share some files between them but I’ve done this in the past found it to be a bat idea, it too easy to have an unfortunate effect on the “other” app. So I decided to copy the current app and have two completely separate folder structures Fitbit-os4
and Fitbit-os5
The first steps are to get the application to build and run
- Update the
package.json
- Rename the
widgets.gui
towidget.defs
, be careful with the letter “s” - Rename the remaining
.gui
files to.view
package.json
So first of all I target the correct devices and OS in the package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"name": "wrist-list",
"version": "2.0.0",
"description": "Fitbit todo list",
"private": true,
"license": "BSD-2-Clause",
"devDependencies": {
"@fitbit/sdk": "^5.0.1",
"@fitbit/sdk-cli": "^1.7.3"
},
"fitbit": {
"appUUID": "0c065eb4-008f-46ed-9929-e1d62c9a11e3",
"appType": "app",
"appDisplayName": "wrist-list",
"iconFile": "resources/images/icon.png",
"wipeColor": "#ffffff",
"requestedPermissions": [
"access_internet",
"access_user_profile",
"run_background"
],
"buildTargets": [
"atlas",
"vulcan"
],
"i18n": {},
"defaultLanguage": "en-US"
},
then npm install
to get the required packages
widget.defs
The migration guide lists the system components that no longer exist, the only option is to remove these from the widgets file so my file went from this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<svg>
<defs>
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="device-specific-styles.css" />
<link rel="import" href="/mnt/sysassets/widgets_common.gui" />
<!-- Additional Imports for views -->
<link rel="import" href="/mnt/sysassets/widgets/baseview_widget.gui" />
<!-- Additional Imports for tile list -->
<link rel="import" href="/mnt/sysassets/widgets/tile_list_widget.gui" />
<!-- Additional Imports for combo buttons -->
<link rel="import" href="/mnt/sysassets/widgets/combo_button_widget.gui" />
<!-- Additional Imports for check boxes -->
<link rel="import" href="/mnt/sysassets/widgets/checkbox_tile_widget.gui" />
<!-- Additional Imports for scroll view -->
<link rel="import" href="/mnt/sysassets/widgets/scrollview_widget.gui" />
<!-- Additional Imports for tumbler view -->
<link rel="import" href="/mnt/sysassets/widgets/tumblerview_widget.gui" />
<!-- Additional Imports for square buttons -->
<link rel="import" href="/mnt/sysassets/widgets/square_button_widget.gui" />
<!-- Mixed text -->
<link rel="stylesheet" href="/mnt/sysassets/widgets/dynamic_textarea.css"/>
<link rel="import" href="/mnt/sysassets/widgets/dynamic_textarea.gui"/>
<link rel="import" href="/mnt/sysassets/widgets/mixed_text_widget.gui"/>
</defs>
</svg>
to this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<svg>
<defs>
<link rel="stylesheet" href="styles.css" />
<link rel="import" href="/mnt/sysassets/system_widget.defs" />
<!-- Additional Imports for views -->
<link rel="import" href="/mnt/sysassets/widgets/baseview_widget.defs" />
<!-- Additional Imports for tile list -->
<link rel="import" href="/mnt/sysassets/widgets/scrollbar.defs" />
<link rel="import" href="/mnt/sysassets/widgets/tile_list_widget.defs" />
<!-- Additional Imports for check boxes -->
<link rel="import" href="/mnt/sysassets/widgets/checkbox.defs" />
<!-- Additional Imports for scroll view -->
<link rel="import" href="/mnt/sysassets/widgets/scrollview_widget.defs" />
<!-- Additional Imports for text buttons -->
<link rel="import" href="/mnt/sysassets/widgets/text_button.defs" />
</defs>
</svg>
there were some quite obvious and documented things that needed to go, the migration guide lists these components as being removed
- panoramaview_widget
- combo_button_widget
- square_button_widget
- push_button_widget
- mixed_text_widget
However what is not documented is that tile_list_widget
is also gone, and with wrist-list being a todo app this was a bit of a problem.
At this point at least the app could be built and would launch, though nothing was displayed on the screen.
Fixing the render
The existing app looks like this
There were a large number of small changes I made to fix the render
- Moved the title bar text towards the middle to cope with the more rounded shape of the device
- Changed the
square-button
to be atext-button
, this was as simple as the migration guide made it sound - Added the SDK recommended styles, I thought this would help with some of the system components
- Ensure that I am only using system font, as its the only supported font in OS5
- Removed the device specific styles, as there is only one screen size for OS5 devices
- Replace the mixed text with some text fields, see below
- Reimplement the checkbox tiles, see below
SDK recommended styles
The SDK recommends adding these styles to the styles.css
1
2
3
4
5
/* SDK recommendations */
.application-fill { fill: fb-cyan; }
.app-gradient-background { fill: fb-blue; }
.foreground-fill { fill: fb-white; }
.background-fill { fill: fb-black; }
Mixed and dynamic text
I replaced the mixed-text
1
2
3
4
5
<use id="no-items-message" href="#mixed-text-center-mid" height="100%" fill="fb-yellow">
<set href="#header/text" attributeName="text-buffer" to="Nothing to do"/>
<set href="#copy/text" attributeName="text-buffer"
to="Add items from the settings page."/>
</use>
with these text
elements
1
2
3
4
<svg class="horizontal-pad">
<text class="h3 center-text application-fill" x="50%" y="50%">Nothing to do</text>
<text class="p3 center-text application-fill" x="50%" y="$">Add items from the settings page.</text>
</svg>
I also replaced the textarea
1
2
<textarea id="about-title" class="about-title" x="5" y="2" width="100%-5" pointer-events="visible">about title...</textarea>
<textarea id="about-copy" class="about-copy" x="5" y="$" width="100%-5" pointer-events="visible">about copy...</textarea>
with a dynamic-textarea
1
2
3
4
5
6
7
8
9
<svg class="horizontal-pad">
<use id="about-title" href="#dynamic-textarea" class="p2 application-fill" x="5" y="20" pointer-events="visible">
<set href="#text" attributeName="text-length" to="50" />
</use>
<rect width="100%" height="2" fill="fb-cyan" y="$+6" />
<use id="about-copy" href="#dynamic-textarea" class="p2 foreground-fill" x="5" y="$+6" pointer-events="visible">
<set href="#text" attributeName="text-length" to="100" />
</use>
</svg>
Implementing checkbox tiles
I did find this posting that gave some information about checkboxes in OS5. However there is no documentation on how to use them so in the end I found that it was just easier to reimplement them myself.
The markup looks like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<svg id="main-screen">
<defs>
<!-- Template Symbol for the checkbox items -->
<symbol id="header-item" href="#tile-list-item" class="list-item-header" height="35" focusable="false" display="none">
<textarea id="item-header-text" x="10" y="4" width="100%" height="100%">header-text</textarea>
</symbol>
<symbol id="tile-item" href="#tile-list-item" class="list-item" height="55" focusable="false" pointer-events="none" system-events="all" display="none">
<rect id="item-background" x="0" y="0" width="100%" height="100%"/>
<textarea id="item-text" x="10" y="11" text-length="50" width="100% - 48" height="100%" fill="white">item-text</textarea>
<rect id="check-rect-border" x="100% - 44" y="13" width="30" height="30" />
<rect id="check-rect" x="100% - 43" y="14" width="28" height="28" />
<image id="check-on-img" x="100% - 43" y="14" width="28" height="28" href="images/check_on.png" />
<rect id="tile-divider-bottom" class="tile-divider-bottom" />
<rect id="touch" pointer-events="all" />
</symbol>
</defs>
<use id="main-list" href="#tile-list">
<var id="virtual" value="1" />
<var id="reorder-enabled" value="0" />
<var id="separator-height-bottom" value="2" />
<!-- peek mode is disabled for checkboxes so lets do it for the list item as well -->
<var id="peek-enabled" value="0" />
<use id="header-pool" href="#tile-list-pool">
<use id="header-pool[0]" href="#header-item" class="tile-list-item" />
<use id="header-pool[1]" href="#header-item" class="tile-list-item" />
<use id="header-pool[2]" href="#header-item" class="tile-list-item" />
<use id="header-pool[3]" href="#header-item" class="tile-list-item" />
<use id="header-pool[4]" href="#header-item" class="tile-list-item" />
<use id="header-pool[5]" href="#header-item" class="tile-list-item" />
<use id="header-pool[6]" href="#header-item" class="tile-list-item" />
<use id="header-pool[7]" href="#header-item" class="tile-list-item" />
<use id="header-pool[8]" href="#header-item" class="tile-list-item" />
<use id="header-pool[9]" href="#header-item" class="tile-list-item" />
</use>
<use id="item-pool" href="#tile-list-pool">
<use id="item-pool[0]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[1]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[2]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[3]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[4]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[5]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[6]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[7]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[8]" href="#tile-item" class="tile-list-item" />
<use id="item-pool[9]" href="#tile-item" class="tile-list-item" />
</use>
</use>
</svg>
I particularly liked the rect
with the id of touch
as a mechanism for adding a click event, which I found in the SDK guide for the tile list component.
The CSS is this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.list-item-header {
font-size: 20;
font-family: System-Regular;
text-length: 32;
fill: #FFFFFF;
}
.tile-divider-bottom {
x: 0;
y: 100%-2;
width: 100%;
height: 2;
fill: #A0A0A0;
}
#touch {
width: 100%;
height: 100%-6;
x: 0;
y: 0;
opacity: 0;
}
The only part I had to make a compromise on was the that in the original app I could have multiline checkboxes, in fact I blogged about it, in this implementation I only support single line with truncation. In fact that was one of the things that is different in OS5 that can trip you up, if you dont specify the height
of the tile-item
symbol (on line 8 of the SVG markup) then nothing is displayed.
As multiline is not supported it made the javascript less complex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const selectedBackgroundColour = "#000000";
const selectedTextColour = "green";
const notSelectedBackgroundColour = "#000000";
const notSelectedTextColour = "#FFFFFF";
function setupTileColours(bg, textArea, checkBoxRectBorder, checkBoxRect, checkImage, selected) {
bg.style.fill = selected ? selectedBackgroundColour : notSelectedBackgroundColour;
textArea.style.fill = selected ? selectedTextColour : notSelectedTextColour;
checkBoxRectBorder.style.fill = selected ? selectedTextColour : notSelectedTextColour;
checkBoxRect.style.fill = selected ? selectedBackgroundColour : notSelectedBackgroundColour;
if (selected) {
checkImage.style.display = "inline";
checkImage.style.fill = selected ? selectedTextColour : notSelectedTextColour;
} else {
checkImage.style.display = "none";
}
}
function setupTileList(data) {
tileList.delegate = {
getTileInfo: (index) => {
const poolType = (data.list[index].type === "header") ? "header-pool" : "item-pool";
return {
type: poolType,
data: data.list[index],
};
},
configureTile: (tile, info) => {
if (info.data.type === "header") {
tile.getElementById("item-header-text").text = info.data.text;
} else {
const bg = tile.getElementById("item-background");
const textArea = tile.getElementById("item-text");
const checkBoxRectBorder = tile.getElementById("check-rect-border");
const checkBoxRect = tile.getElementById("check-rect");
const checkImage = tile.getElementById("check-on-img");
textArea.text = info.data.text;
setupTileColours(bg,textArea,checkBoxRectBorder,checkBoxRect,checkImage,info.data.selected);
let touch = tile.getElementById("touch");
touch.onclick = (evt) => {
info.data.selected = !info.data.selected;
setupTileColours(bg,textArea,checkBoxRectBorder,checkBoxRect,checkImage,info.data.selected);
updateTitleBar(data);
};
}
}
};
}
And after all these changes it looks like this
The tick image is from the Fitbit SDK design assets.
Unresolved items
I am still getting this error in the simulator
App: Error 22 Load event was not sent due to missing type handler '(null)' in ./Resources/switcher/switcher_main.view
and sometimes this error
App: Error 2 Invalid path '/mnt/sysassets/widgets/images/fb_logo/logo_dot_a8.png'
But as far as I can tell this is just the simulator being a bit flaky.