Skip to content

Commit 6f4d97f

Browse files
committed
feat: add more google drive file metadata, add Unsaved sync status
1 parent a96604d commit 6f4d97f

9 files changed

Lines changed: 209 additions & 111 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@
1717
# 6. Copy the generated Client ID and paste it below.
1818
#
1919
VITE_GOOGLE_CLIENT_ID=
20+
21+
# Sync delay in milliseconds (default: 3000)
22+
# VITE_SYNC_DELAY_MS=3000

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ updates:
88
- package-ecosystem: 'npm' # See documentation for possible values
99
directory: '/' # Location of package manifests
1010
schedule:
11-
interval: 'weekly'
11+
interval: 'weekly'

src/lib/components/Navbar.svelte

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
CloudOff,
99
CloudCog,
1010
AlertCircle,
11+
Clock,
1112
Home,
1213
FolderHeart,
1314
Settings
@@ -50,20 +51,29 @@
5051
: ''}
5152
>
5253
<div
53-
class="mr-2 badge sm:mr-4 inline-grid place-items-center transition-colors duration-300 {dbStore.syncStatus === 'Synced'
54+
class="mr-2 badge inline-grid place-items-center transition-colors duration-300 sm:mr-4 {dbStore.syncStatus ===
55+
'Synced'
5456
? 'badge-outline badge-success'
5557
: dbStore.syncStatus === 'Syncing...'
5658
? 'badge-outline badge-info'
57-
: dbStore.syncStatus === 'Error'
58-
? 'badge-outline badge-error'
59-
: 'badge-ghost'}"
59+
: dbStore.syncStatus === 'Unsaved'
60+
? 'badge-outline badge-warning'
61+
: dbStore.syncStatus === 'Error'
62+
? 'badge-outline badge-error'
63+
: 'badge-ghost'}"
6064
>
6165
{#key dbStore.syncStatus}
62-
<div class="col-start-1 row-start-1 flex items-center justify-center" in:scale={{ duration: 300, start: 0.8, delay: 150 }} out:fade={{ duration: 150 }}>
66+
<div
67+
class="col-start-1 row-start-1 flex items-center justify-center"
68+
in:scale={{ duration: 300, start: 0.8, delay: 150 }}
69+
out:fade={{ duration: 150 }}
70+
>
6371
{#if dbStore.syncStatus === 'Synced'}
6472
<Cloud class="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
6573
{:else if dbStore.syncStatus === 'Syncing...'}
6674
<CloudCog class="mr-1 h-3 w-3 animate-pulse sm:h-4 sm:w-4" />
75+
{:else if dbStore.syncStatus === 'Unsaved'}
76+
<Clock class="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
6777
{:else if dbStore.syncStatus === 'Error'}
6878
<AlertCircle class="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
6979
{:else}

src/lib/components/SnippetCard.svelte

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
let editContents = $state<Record<string, string>>(startInEditMode ? { ...snippet.content } : {});
2525
let deleteModal: HTMLDialogElement;
2626
27-
2827
let languagesShowingInMultiple = $derived(
2928
dbStore.data.settings.languages.filter((l) => l.showInMultiple)
3029
);
@@ -163,20 +162,24 @@
163162

164163
<div
165164
class={`snippet-card group card border border-base-200 bg-base-100 shadow-xl ${isReorderMode ? 'shake-animation cursor-grab active:cursor-grabbing' : ''} ${isSlim ? 'md:overflow-hidden' : ''}`}
166-
style={isReorderMode ? `animation-delay: -${Math.random() * 0.3}s; animation-duration: ${0.25 + Math.random() * 0.15}s;` : ''}
165+
style={isReorderMode
166+
? `animation-delay: -${Math.random() * 0.3}s; animation-duration: ${0.25 + Math.random() * 0.15}s;`
167+
: ''}
167168
data-id={snippet.id}
168169
transition:scale={{ duration: 300, start: 0.9, opacity: 0, easing: backOut }}
169170
>
170171
<div class={`pointer-events-none card-body p-4 ${isSlim ? 'md:p-2' : ''}`}>
171172
<div
172-
class={`flex items-start justify-between ${isReorderMode ? 'opacity-50' : 'pointer-events-auto'} ${isSlim ? 'md:absolute md:top-0 md:left-0 md:right-0 md:z-10 md:transition-all md:duration-200 md:bg-linear-to-b md:from-base-100/95 md:to-transparent md:p-2 md:pb-6 md:pointer-events-none' : ''} ${isSlim && !isEditing ? `md:-translate-y-full md:opacity-0 ${!isReorderMode ? 'md:group-hover:translate-y-0 md:group-hover:opacity-100' : ''}` : ''} ${isSlim && isEditing ? 'md:translate-y-0 md:opacity-100' : ''}`}
173+
class={`flex items-start justify-between ${isReorderMode ? 'opacity-50' : 'pointer-events-auto'} ${isSlim ? 'md:pointer-events-none md:absolute md:top-0 md:right-0 md:left-0 md:z-10 md:bg-linear-to-b md:from-base-100/95 md:to-transparent md:p-2 md:pb-6 md:transition-all md:duration-200' : ''} ${isSlim && !isEditing ? `md:-translate-y-full md:opacity-0 ${!isReorderMode ? 'md:group-hover:translate-y-0 md:group-hover:opacity-100' : ''}` : ''} ${isSlim && isEditing ? 'md:translate-y-0 md:opacity-100' : ''}`}
173174
>
174175
<div class="pointer-events-auto flex flex-col gap-1">
175176
<div
176177
class="tooltip tooltip-bottom tooltip-right"
177178
data-tip={new Date(snippet.updatedAt).toLocaleString()}
178179
>
179-
<div class={`${isSlim ? 'md:mt-0' : 'mt-1'} flex w-max cursor-help items-center gap-1 text-xs text-base-content/50`}>
180+
<div
181+
class={`${isSlim ? 'md:mt-0' : 'mt-1'} flex w-max cursor-help items-center gap-1 text-xs text-base-content/50`}
182+
>
180183
<Clock class="h-3 w-3" />
181184
Updated {getRelativeTime(snippet.updatedAt)}
182185
</div>
@@ -214,7 +217,7 @@
214217
</div>
215218
{:else}
216219
<button
217-
class="pointer-events-auto absolute right-0 btn btn-circle btn-ghost btn-sm"
220+
class="btn pointer-events-auto absolute right-0 btn-circle btn-ghost btn-sm"
218221
in:fade={{ duration: 150 }}
219222
out:fade={{ duration: 150 }}
220223
onclick={startEdit}
@@ -240,19 +243,19 @@
240243
in:fade={{ duration: 200, delay: 50 }}
241244
out:fade={{ duration: 150 }}
242245
>
243-
{#each dbStore.data.settings.languages as lang (lang.id)}
244-
<div>
245-
<label class="label pt-0 pb-1" for="content-{snippet.id}-{lang.id}"
246-
><span class="label-text text-xs font-bold">{lang.name} Content</span></label
247-
>
248-
<textarea
249-
id="content-{snippet.id}-{lang.id}"
250-
class="textarea-bordered textarea w-full font-mono text-sm leading-relaxed"
251-
rows="3"
252-
bind:value={editContents[lang.id]}
253-
placeholder="Enter {lang.name} snippet..."></textarea>
254-
</div>
255-
{/each}
246+
{#each dbStore.data.settings.languages as lang (lang.id)}
247+
<div>
248+
<label class="label pt-0 pb-1" for="content-{snippet.id}-{lang.id}"
249+
><span class="label-text text-xs font-bold">{lang.name} Content</span></label
250+
>
251+
<textarea
252+
id="content-{snippet.id}-{lang.id}"
253+
class="textarea-bordered textarea w-full font-mono text-sm leading-relaxed"
254+
rows="3"
255+
bind:value={editContents[lang.id]}
256+
placeholder="Enter {lang.name} snippet..."></textarea>
257+
</div>
258+
{/each}
256259
</div>
257260
</div>
258261
{:else}
@@ -266,10 +269,13 @@
266269
{@const content = snippet.content[lang.id]}
267270
{#if content || !isMultipleMode}
268271
<div
269-
class="flex flex-col gap-1 {isMultipleMode && dbStore.data.settings.hideLanguageTitles
272+
class="flex flex-col gap-1 {isMultipleMode &&
273+
dbStore.data.settings.hideLanguageTitles
270274
? 'tooltip tooltip-top before:z-50 hover:before:delay-1000 hover:after:delay-1000'
271275
: ''}"
272-
data-tip={isMultipleMode && dbStore.data.settings.hideLanguageTitles ? lang.name : null}
276+
data-tip={isMultipleMode && dbStore.data.settings.hideLanguageTitles
277+
? lang.name
278+
: null}
273279
>
274280
{#if isMultipleMode && !dbStore.data.settings.hideLanguageTitles}
275281
<span class="px-1 text-xs font-bold opacity-60">{lang.name}</span>
@@ -289,7 +295,12 @@
289295
>
290296
<div
291297
class="flex items-center gap-2 rounded-full bg-success px-3 py-1 font-sans font-bold text-success-content shadow-sm"
292-
transition:scale={{ duration: 300, start: 0.8, opacity: 0, easing: backOut }}
298+
transition:scale={{
299+
duration: 300,
300+
start: 0.8,
301+
opacity: 0,
302+
easing: backOut
303+
}}
293304
>
294305
<Check class="h-4 w-4" /> Copied
295306
</div>
@@ -298,7 +309,7 @@
298309
</button>
299310
{:else}
300311
<button
301-
class="relative block min-h-12 w-full cursor-default select-none rounded-lg bg-base-200 p-3 text-left font-mono text-sm wrap-break-word whitespace-pre-wrap text-base-content/50 italic transition-colors"
312+
class="relative block min-h-12 w-full cursor-default rounded-lg bg-base-200 p-3 text-left font-mono text-sm wrap-break-word whitespace-pre-wrap text-base-content/50 italic transition-colors select-none"
302313
disabled={true}
303314
>
304315
Empty snippet
@@ -309,7 +320,7 @@
309320
{/each}
310321
{#if isMultipleMode && displayedLanguages.every((lang) => !snippet.content[lang.id])}
311322
<button
312-
class="relative block min-h-12 w-full cursor-default select-none rounded-lg bg-base-200 p-3 text-left font-mono text-sm wrap-break-word whitespace-pre-wrap text-base-content/50 italic transition-colors"
323+
class="relative block min-h-12 w-full cursor-default rounded-lg bg-base-200 p-3 text-left font-mono text-sm wrap-break-word whitespace-pre-wrap text-base-content/50 italic transition-colors select-none"
313324
disabled={true}
314325
>
315326
Empty snippet

src/lib/stores/auth.svelte.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ class AuthStore {
1313
});
1414

1515
gdrive = $state({
16-
clientId: browser ? localStorage.getItem('gd_client_id') || import.meta.env.VITE_GOOGLE_CLIENT_ID || '' : '',
16+
clientId: browser
17+
? localStorage.getItem('gd_client_id') || import.meta.env.VITE_GOOGLE_CLIENT_ID || ''
18+
: '',
1719
accessToken: browser ? localStorage.getItem('gd_access_token') || '' : '',
1820
fileId: browser ? localStorage.getItem('gd_file_id') || '' : ''
1921
});
2022

2123
save() {
2224
if (browser) {
2325
localStorage.setItem('cp_active_provider', this.activeProvider);
24-
26+
2527
// Github
2628
localStorage.setItem('gh_pat', this.github.token);
2729
localStorage.setItem('gh_gist_id', this.github.gistId);

src/lib/stores/db.svelte.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ class DbStore {
8686
conflictData = $state<Database | null>(null);
8787
isLoading = $state(false);
8888
error = $state<string | null>(null);
89-
syncStatus = $state<'Offline/No Auth' | 'Syncing...' | 'Synced' | 'Error' | 'Local Only'>(
90-
authStore.isValid ? 'Syncing...' : 'Local Only'
91-
);
89+
syncStatus = $state<
90+
'Offline/No Auth' | 'Unsaved' | 'Syncing...' | 'Synced' | 'Error' | 'Local Only'
91+
>(authStore.isValid ? 'Syncing...' : 'Local Only');
9292

9393
setGlobalLanguageId(id: string) {
9494
this.globalLanguageId = id;
@@ -147,10 +147,10 @@ class DbStore {
147147
this.isLoading = true;
148148
this.syncStatus = 'Syncing...';
149149
this.error = null;
150-
150+
151151
try {
152152
const result = await adapter.sync(this.data, this.getLastSyncedAt());
153-
153+
154154
if (result.action === 'error') {
155155
this.error = result.error || 'Unknown sync error';
156156
this.syncStatus = 'Error';
@@ -190,10 +190,17 @@ class DbStore {
190190
clearTimeout(this._saveTimeout);
191191
}
192192

193+
let syncDelay = parseInt(import.meta.env.VITE_SYNC_DELAY_MS || '3000', 10);
194+
if (isNaN(syncDelay)) syncDelay = 3000;
195+
196+
if (authStore.isValid) {
197+
this.syncStatus = 'Unsaved';
198+
}
199+
193200
this._saveTimeout = setTimeout(() => {
194201
this._saveTimeout = null;
195202
this._pushToRemote();
196-
}, 1000);
203+
}, syncDelay);
197204
}
198205

199206
async forcePush() {
@@ -203,7 +210,7 @@ class DbStore {
203210

204211
forcePull(remoteData: Database) {
205212
if (remoteData?.settings?.languages) {
206-
remoteData.settings.languages.forEach((l: any) => {
213+
remoteData.settings.languages.forEach((l: Language) => {
207214
if (l.showInMultiple === undefined) {
208215
l.showInMultiple = l.id === 'en' || l.id === 'de';
209216
}
@@ -235,7 +242,7 @@ class DbStore {
235242
this.isLoading = true;
236243
this.syncStatus = 'Syncing...';
237244
this.error = null;
238-
245+
239246
try {
240247
const result = await adapter.push(this.data);
241248
this.setLastSyncedAt(result.remoteDate);

src/lib/stores/providers/gdrive.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Database } from '../db.svelte';
1+
import type { Database, Language } from '../db.svelte';
22
import type { StorageAdapter, SyncResult } from './types';
33
import { authStore } from '../auth.svelte';
44

@@ -54,7 +54,7 @@ export class GDriveProvider implements StorageAdapter {
5454

5555
// Migration for older data formats
5656
if (remoteData?.settings?.languages) {
57-
remoteData.settings.languages.forEach((l: any) => {
57+
remoteData.settings.languages.forEach((l: Language) => {
5858
if (l.showInMultiple === undefined) {
5959
l.showInMultiple = l.id === 'en' || l.id === 'de';
6060
}
@@ -155,7 +155,7 @@ export class GDriveProvider implements StorageAdapter {
155155

156156
if (!res.ok) throw new Error('Failed to update Google Drive AppData');
157157
const data = await res.json();
158-
158+
159159
if (!fileId && data.id) {
160160
authStore.gdrive.fileId = data.id;
161161
authStore.save();

src/lib/stores/providers/github.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Database } from '../db.svelte';
1+
import type { Database, Language } from '../db.svelte';
22
import type { StorageAdapter, SyncResult } from './types';
33
import { authStore } from '../auth.svelte';
44

@@ -29,7 +29,7 @@ export class GithubProvider implements StorageAdapter {
2929

3030
// Migration for older data formats
3131
if (remoteData?.settings?.languages) {
32-
remoteData.settings.languages.forEach((l: any) => {
32+
remoteData.settings.languages.forEach((l: Language) => {
3333
if (l.showInMultiple === undefined) {
3434
l.showInMultiple = l.id === 'en' || l.id === 'de';
3535
}

0 commit comments

Comments
 (0)