Skip to content

Commit f1ef8ee

Browse files
committed
feat: support for persistent store in browser
1 parent 102e6c7 commit f1ef8ee

File tree

6 files changed

+166
-53
lines changed

6 files changed

+166
-53
lines changed

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,17 @@ Devbox is a lightweight, cross-platform desktop application built with Tauri (Ru
5858
- [x] Timezone
5959
- [ ] Diff tools
6060
- [ ] CSS Playground
61-
- [ ] Color Testing
6261
- [ ] Quick Type
63-
- [ ] Password Generator
6462
- [ ] Stateless password
6563
- [ ] Lorem Ipsum
66-
- [ ] Harmonies
6764
- [ ] Faker
6865
- [ ] HTML Formatter
6966
- [ ] CSS Formatter
7067
- [ ] JS/TS Formatter
7168
- [ ] SQL Formatter
7269
- [ ] HTML Preview
7370
- [ ] Base64 Text (encode/decode)
74-
- [ ] Color Utils
7571
- [ ] JSON <> YAML
7672
- [ ] Hashing Text
77-
- [ ] Hasing Files
7873
- [ ] WebSocket Client
7974
- [ ] Mock API Server / Webhook test

src/App.module.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
}
2121

2222
.buttonRoot {
23-
&[data-variant='danger'] {
23+
&[data-variant="danger"] {
2424
background-color: var(--mantine-color-red-8);
2525
color: var(--mantine-color-white);
2626
&:hover {
2727
background-color: var(--mantine-color-red-9);
2828
}
2929
}
3030
/* Override default disabled styles */
31-
&[data-variant='danger']:disabled {
31+
&[data-variant="danger"]:disabled {
3232
background-color: transparent;
3333
color: var(--mantine-color-white);
3434
opacity: 0.5;

src/Components/Settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function Settings() {
1313
const [sidebarConfig, setSidebarConfig] = useLocalStorage<SidebarConfig>({
1414
key: "sidebarConfig",
1515
defaultValue: {
16-
showDescription: false,
16+
showDescription: true,
1717
showModules: true,
1818
hiddenTools: [],
1919
},

src/Components/Sidebar/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export const Sidebar = ({ collapsed, setCollapsed }: Props) => {
3636
const [sidebarConfig] = useLocalStorage<SidebarConfig>({
3737
key: "sidebarConfig",
3838
defaultValue: {
39-
showDescription: false,
40-
showModules: false,
39+
showDescription: true,
40+
showModules: true,
4141
hiddenTools: [],
4242
},
4343
});

src/Features/timezone/components/styles.module.css

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,76 @@
66
}
77

88
.sliderTicks {
9-
position: absolute;
10-
left: 0;
11-
top: 10px;
12-
bottom: 0;
13-
margin-left: 1.5px;
14-
margin-right: 1.5px;
15-
pointer-events: none;
16-
height: 12px;
17-
display: flex;
18-
width: 100%;
19-
align-items: center;
20-
flex-direction: row;
9+
position: absolute;
10+
left: 0;
11+
top: 10px;
12+
bottom: 0;
13+
margin-left: 1.5px;
14+
margin-right: 1.5px;
15+
pointer-events: none;
16+
height: 12px;
17+
display: flex;
18+
width: 100%;
19+
align-items: center;
20+
flex-direction: row;
2121
}
2222

2323
.sliderTick {
24-
width: calc(1.04167%);
25-
border-color: var(--mantine-color-dark-2);
26-
border-left-width: 1px;
27-
border-left-style: solid;
28-
height: 100%;
24+
width: calc(1.04167%);
25+
border-color: var(--mantine-color-dark-2);
26+
border-left-width: 1px;
27+
border-left-style: solid;
28+
height: 100%;
2929
}
3030

3131
.sliderTickMinor {
32-
width: calc(1.04167%);
33-
border-color: var(--mantine-color-dark-2);
34-
border-left-width: 1px;
35-
border-left-style: solid;
36-
height: 60%;
32+
width: calc(1.04167%);
33+
border-color: var(--mantine-color-dark-2);
34+
border-left-width: 1px;
35+
border-left-style: solid;
36+
height: 60%;
3737
}
3838

3939
.sliderMark {
40-
display: none !important;
40+
display: none !important;
4141
}
4242

4343
.sliderMarkWrapper {
44-
margin-top: 12px;
44+
margin-top: 12px;
4545
}
4646

4747
.slideBar {
48-
background-color: transparent !important;
48+
background-color: transparent !important;
4949
}
5050

5151
.slideThumb {
52-
background-color: var(--mantine-color-gray-4) !important;
53-
width: 24px !important;
54-
height: 24px !important;
55-
margin: 0;
56-
border: none;
52+
background-color: var(--mantine-color-gray-4) !important;
53+
width: 24px !important;
54+
height: 24px !important;
55+
margin: 0;
56+
border: none;
5757
}
5858
.slideThumb:hover {
59-
background-color: var(--mantine-color-gray-1) !important;
59+
background-color: var(--mantine-color-gray-1) !important;
6060
}
6161

6262
.sliderMarkLabel {
63-
color: var(--mantine-color-dark-2) !important;
63+
color: var(--mantine-color-dark-2) !important;
6464
}
6565

6666
.sliderBar {
67-
background-color: transparent !important;
67+
background-color: transparent !important;
6868
}
6969

7070
.timeSeparator {
71-
animation: blink-animation 1s linear infinite;
71+
animation: blink-animation 1s linear infinite;
7272
}
7373

7474
@keyframes blink-animation {
75-
0% {
76-
opacity: 1;
77-
}
78-
50% {
79-
opacity: 0;
80-
}
75+
0% {
76+
opacity: 1;
77+
}
78+
50% {
79+
opacity: 0;
80+
}
8181
}

src/utils/store.ts

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,123 @@ export const AVAILABLE_SETTINGS_KEYS: SettingsKey[] = Object.keys(
1717
DEFAULT_SETTINGS_VALUES
1818
) as SettingsKey[];
1919

20+
// Generic async key-value store interface to abstract storage backends
21+
interface KeyValueStore {
22+
get<T = unknown>(key: string): Promise<T | null>;
23+
set<T = unknown>(key: string, value: T): Promise<void>;
24+
save(): Promise<void>;
25+
has(key: string): Promise<boolean>;
26+
delete(key: string): Promise<void>;
27+
clear(): Promise<void>;
28+
length(): Promise<number>;
29+
}
30+
31+
// localStorage-based implementation as a browser fallback
32+
class LocalStorageStore implements KeyValueStore {
33+
private readonly keyPrefix: string;
34+
private readonly isBrowser: boolean;
35+
36+
constructor(keyPrefix: string = "settings:") {
37+
this.keyPrefix = keyPrefix;
38+
this.isBrowser = typeof window !== "undefined" && typeof window.localStorage !== "undefined";
39+
}
40+
41+
private toFullKey(key: string): string {
42+
return `${this.keyPrefix}${key}`;
43+
}
44+
45+
async get<T = unknown>(key: string): Promise<T | null> {
46+
if (!this.isBrowser) return null;
47+
try {
48+
const raw = window.localStorage.getItem(this.toFullKey(key));
49+
if (raw === null) return null;
50+
return JSON.parse(raw) as T;
51+
} catch (_err) {
52+
return null;
53+
}
54+
}
55+
56+
async set<T = unknown>(key: string, value: T): Promise<void> {
57+
if (!this.isBrowser) return;
58+
try {
59+
window.localStorage.setItem(this.toFullKey(key), JSON.stringify(value));
60+
} catch (_err) {
61+
// ignore quota or serialization errors
62+
}
63+
}
64+
65+
async save(): Promise<void> {
66+
// No-op for localStorage
67+
return;
68+
}
69+
70+
async has(key: string): Promise<boolean> {
71+
if (!this.isBrowser) return false;
72+
return window.localStorage.getItem(this.toFullKey(key)) !== null;
73+
}
74+
75+
async delete(key: string): Promise<void> {
76+
if (!this.isBrowser) return;
77+
try {
78+
window.localStorage.removeItem(this.toFullKey(key));
79+
} catch (_err) {
80+
// ignore
81+
}
82+
}
83+
84+
async clear(): Promise<void> {
85+
if (!this.isBrowser) return;
86+
try {
87+
const keysToRemove: string[] = [];
88+
for (let i = 0; i < window.localStorage.length; i++) {
89+
const k = window.localStorage.key(i);
90+
if (k && k.startsWith(this.keyPrefix)) {
91+
keysToRemove.push(k);
92+
}
93+
}
94+
keysToRemove.forEach(k => window.localStorage.removeItem(k));
95+
} catch (_err) {
96+
// ignore
97+
}
98+
}
99+
100+
async length(): Promise<number> {
101+
if (!this.isBrowser) return 0;
102+
try {
103+
let count = 0;
104+
for (let i = 0; i < window.localStorage.length; i++) {
105+
const k = window.localStorage.key(i);
106+
if (k && k.startsWith(this.keyPrefix)) count++;
107+
}
108+
return count;
109+
} catch (_err) {
110+
return 0;
111+
}
112+
}
113+
}
114+
20115
/**
21116
* Application settings store
22117
*/
23118
class SettingsStore {
24-
private store: LazyStore;
119+
private store: KeyValueStore;
25120

26121
constructor() {
27-
this.store = new LazyStore("settings.json");
122+
this.store = this.createInitialStore();
28123
this.initialize();
29124
}
30125

126+
private createInitialStore(): KeyValueStore {
127+
try {
128+
if (typeof window !== "undefined" && (window as any).__TAURI__) {
129+
return new LazyStore("settings.json") as unknown as KeyValueStore;
130+
}
131+
} catch (_err) {
132+
// ignore and fallback to localStorage
133+
}
134+
return new LocalStorageStore("settings:");
135+
}
136+
31137
private async initialize(): Promise<void> {
32138
try {
33139
const isEmpty = !(await this.store.length());
@@ -39,11 +145,23 @@ class SettingsStore {
39145
}
40146
}
41147

42-
if (import.meta.env.DEV) {
148+
if (import.meta.env.DEV && typeof window !== "undefined") {
43149
(window as any).settingsStore = this;
44150
}
45151
} catch (error) {
46152
console.error("Failed to initialize settings store:", error);
153+
// Fallback to localStorage store if tauri store fails at runtime
154+
if (!(this.store instanceof LocalStorageStore)) {
155+
this.store = new LocalStorageStore("settings:");
156+
try {
157+
const isEmpty = !(await this.store.length());
158+
if (isEmpty) {
159+
await this.setDefaults();
160+
}
161+
} catch (fallbackError) {
162+
console.error("Failed to initialize fallback localStorage store:", fallbackError);
163+
}
164+
}
47165
}
48166
}
49167

0 commit comments

Comments
 (0)