Skip to content

Commit d5c637d

Browse files
committed
feat: Add settings sync
1 parent 82495d5 commit d5c637d

File tree

4 files changed

+182
-12
lines changed

4 files changed

+182
-12
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ What started as an experiment to play with the Chrome browser APIs and explore w
2121
- Distraction-free, minimal design aesthetic with multiple themes.
2222
- A list of your open tabs, recently closed tabs, and top sites.
2323
- Search tabs, bookmarks, history, and top sites in one place.
24-
- Simple bookmarks bar.
2524
- Links to frequently used destinations in your browser.
25+
- Simple bookmarks bar.
26+
- Customisable UI.
27+
- Optional automatic or manual settings sync between browsers.
2628

2729
### Design goals
2830

29-
<!-- prettier-ignore -->
3031
| Issue | Why / How |
3132
| --- | --- |
3233
| Access | Still have access to common things like the bookmarks bar etc. |

src/settings.ts

+137-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { append, clone, collect, h } from 'stage1';
22
import { compile } from 'stage1/macro' with { type: 'macro' };
33
import { reconcile } from 'stage1/reconcile/non-keyed';
4-
import type { SectionOrderItem, ThemesData } from './types';
4+
import type {
5+
SectionOrderItem,
6+
SyncStorageData,
7+
ThemesData,
8+
UserStorageData,
9+
} from './types';
510
import { DEFAULT_SECTION_ORDER, storage } from './utils';
611

712
// TODO: Show errors in the UI.
@@ -10,6 +15,7 @@ import { DEFAULT_SECTION_ORDER, storage } from './utils';
1015

1116
interface SettingsState {
1217
order: [SectionOrderItem[], SectionOrderItem[]];
18+
pushSyncData?(forceUpdate?: boolean): Promise<void>;
1319
}
1420

1521
type ItemIndex = [listIndex: 0 | 1, itemIndex: number];
@@ -28,6 +34,19 @@ const themesData = fetch('themes.json').then(
2834
(response) => response.json() as Promise<ThemesData>,
2935
);
3036

37+
const supportsSync = async (): Promise<boolean> => {
38+
try {
39+
await chrome.storage.sync.set({ _: 1 });
40+
await chrome.storage.sync.remove('_');
41+
if (chrome.runtime.lastError) {
42+
return false;
43+
}
44+
return true;
45+
} catch {
46+
return false;
47+
}
48+
};
49+
3150
type SectionComponent = HTMLLIElement;
3251

3352
interface SectionRefs {
@@ -102,17 +121,23 @@ const SectionItem = (
102121
};
103122

104123
interface Refs {
105-
feedback: HTMLDivElement;
124+
feedback: Text;
106125
theme: HTMLSelectElement;
107126
b: HTMLInputElement;
108127
se: HTMLUListElement;
109128
sd: HTMLUListElement;
110129
reset: HTMLButtonElement;
130+
131+
feedback2: Text;
132+
sync: HTMLInputElement;
133+
pull: HTMLButtonElement;
134+
push: HTMLButtonElement;
135+
clear: HTMLButtonElement;
111136
}
112137

113138
const meta = compile(`
114139
<div>
115-
<div @feedback></div>
140+
<div>@feedback</div>
116141
117142
<div class=row>
118143
<label>Theme</label>
@@ -151,6 +176,29 @@ const meta = compile(`
151176
<label>Reset</label>
152177
<button @reset>Reset all settings</button>
153178
</div>
179+
180+
<hr>
181+
182+
<h2>Experimental</h2>
183+
184+
<h3>Sync Settings</h3>
185+
186+
<div class=row>
187+
@feedback2
188+
</div>
189+
190+
<div class=row>
191+
<label>
192+
<input @sync type=checkbox class=box disabled> Automatically sync settings
193+
</label>
194+
<small class=muted>Sync on profile startup (requires sign-in)</small>
195+
</div>
196+
197+
<div class=row>
198+
<button @pull disabled>Pull now (local ⟸ sync)</button>
199+
<button @push disabled>Push now (local ⟹ sync)</button>
200+
<button @clear disabled>Reset sync data</button>
201+
</div>
154202
</div>
155203
`);
156204
const view = h<HTMLDivElement>(meta.html);
@@ -192,8 +240,10 @@ const Settings = () => {
192240
});
193241

194242
if (themeName === DEFAULT_THEME) {
195-
void chrome.storage.local.remove('tn');
243+
await chrome.storage.local.remove('tn');
196244
}
245+
246+
void state.pushSyncData?.();
197247
};
198248

199249
const updateOrder = (order: SettingsState['order'], skipSave?: boolean) => {
@@ -214,6 +264,8 @@ const Settings = () => {
214264
o: order[0],
215265
});
216266
}
267+
268+
void state.pushSyncData?.();
217269
}
218270
};
219271

@@ -234,15 +286,18 @@ const Settings = () => {
234286

235287
refs.theme.onchange = () => updateTheme(refs.theme.value);
236288

237-
refs.b.onchange = () => {
289+
refs.b.onchange = async () => {
290+
// eslint-disable-next-line unicorn/prefer-ternary
238291
if (refs.b.checked) {
239292
// When value is same as default, we don't need to store it
240-
void chrome.storage.local.remove('b');
293+
await chrome.storage.local.remove('b');
241294
} else {
242-
void chrome.storage.local.set({
295+
await chrome.storage.local.set({
243296
b: true,
244297
});
245298
}
299+
300+
void state.pushSyncData?.();
246301
};
247302

248303
// eslint-disable-next-line no-multi-assign
@@ -266,10 +321,84 @@ const Settings = () => {
266321
(item) => !orderEnabled.includes(item),
267322
);
268323

269-
void updateTheme(themeName);
324+
refs.theme.value = themeName;
270325
refs.b.checked = !storage.b;
271326
updateOrder([orderEnabled, orderDisabled], true);
272327

328+
/* ********************************** */
329+
// Experimental sync settings feature //
330+
/* ********************************** */
331+
332+
refs.sync.checked = !!storage.s;
333+
334+
const updateSync = (syncData: SyncStorageData) => {
335+
if (syncData.ts) {
336+
refs.feedback2.nodeValue = `Sync data found (last updated: ${new Date(
337+
syncData.ts,
338+
).toLocaleString()})`;
339+
refs.pull.disabled = false;
340+
refs.clear.disabled = false;
341+
} else {
342+
refs.feedback2.nodeValue = 'No sync data found';
343+
refs.pull.disabled = true;
344+
refs.clear.disabled = true;
345+
}
346+
347+
refs.push.disabled = false;
348+
refs.sync.disabled = false;
349+
350+
refs.sync.onchange = () => {
351+
if (refs.sync.checked) {
352+
void chrome.storage.local.set({
353+
s: true,
354+
});
355+
// @ts-expect-error - doesn't need event argument
356+
refs.pull.onclick?.();
357+
} else {
358+
void chrome.storage.local.remove('s');
359+
}
360+
};
361+
362+
refs.pull.onclick = () => {
363+
if (syncData.data) {
364+
void chrome.storage.local.set(syncData.data);
365+
void updateTheme(syncData.data.tn ?? DEFAULT_THEME);
366+
updateOrder([syncData.data.o ?? [...DEFAULT_SECTION_ORDER], []], true);
367+
}
368+
};
369+
370+
state.pushSyncData = async (forceUpdate?: boolean) => {
371+
const { t, s, ...rest } =
372+
await chrome.storage.local.get<UserStorageData>();
373+
374+
if (forceUpdate || s) {
375+
const newSyncData: SyncStorageData = {
376+
data: rest,
377+
ts: Date.now(),
378+
};
379+
void chrome.storage.sync.set(newSyncData);
380+
updateSync(newSyncData);
381+
}
382+
};
383+
384+
refs.push.onclick = () => state.pushSyncData!(true);
385+
386+
refs.clear.onclick = () => {
387+
void chrome.storage.sync.clear();
388+
updateSync({});
389+
};
390+
};
391+
392+
void supportsSync().then((canSync) => {
393+
if (canSync) {
394+
void chrome.storage.sync.get<SyncStorageData>().then(updateSync);
395+
// TODO: Listen for sync data changes?
396+
// chrome.storage.sync.onChanged.addListener((changes) => {});
397+
} else {
398+
refs.feedback2.nodeValue = 'Not signed in or sync not supported';
399+
}
400+
});
401+
273402
return root;
274403
};
275404

src/sw.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/// <reference lib="webworker" />
22

3-
import type { ThemesData, UserStorageData } from './types';
3+
import type { SyncStorageData, ThemesData, UserStorageData } from './types';
44

55
// On install or subsequent update, preload the user's chosen theme into storage
66
// eslint-disable-next-line @typescript-eslint/no-misused-promises
77
chrome.runtime.onInstalled.addListener(async () => {
88
const [themes, settings] = await Promise.all([
99
fetch('themes.json').then((res) => res.json() as Promise<ThemesData>),
10-
chrome.storage.local.get<UserStorageData>(),
10+
chrome.storage.local.get<UserStorageData>('tn'),
1111
]);
1212

1313
// TODO: Remove once most users have updated.
@@ -30,3 +30,34 @@ chrome.runtime.onInstalled.addListener(async () => {
3030
// });
3131
// }
3232
});
33+
34+
/* ********************************** */
35+
// Experimental sync settings feature //
36+
/* ********************************** */
37+
38+
// On profile startup, pull remote user settings; local <- sync
39+
chrome.runtime.onStartup.addListener(() => {
40+
void chrome.storage.local
41+
.get<UserStorageData>(['s', 'tn'])
42+
.then((settings) => {
43+
// Only when sync is enabled
44+
if (!settings.s) return;
45+
46+
void chrome.storage.sync.get<SyncStorageData>().then((remote) => {
47+
if (remote.data) {
48+
if (remote.data.tn === settings.tn) {
49+
void chrome.storage.local.set(remote.data);
50+
} else {
51+
void fetch('themes.json')
52+
.then((res) => res.json() as Promise<ThemesData>)
53+
.then((themes) => {
54+
void chrome.storage.local.set({
55+
t: themes[settings.tn ?? 'auto'],
56+
...remote.data,
57+
});
58+
});
59+
}
60+
}
61+
});
62+
});
63+
});

src/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,13 @@ export interface UserStorageData {
2222
b?: boolean;
2323
/** Sections order user preference. */
2424
o?: SectionOrderItem[];
25+
/** Settings sync enabled user preference. */
26+
s?: boolean;
27+
}
28+
29+
export interface SyncStorageData {
30+
/** User settings data. */
31+
data?: Omit<UserStorageData, 't' | 's'>;
32+
/** Timestamp of the last sync.set operation. */
33+
ts?: number;
2534
}

0 commit comments

Comments
 (0)