diff --git a/e2e/tests/planner/planner-add-subtask-from-detail.spec.ts b/e2e/tests/planner/planner-add-subtask-from-detail.spec.ts index 5272e565bc..32eb772d8c 100644 --- a/e2e/tests/planner/planner-add-subtask-from-detail.spec.ts +++ b/e2e/tests/planner/planner-add-subtask-from-detail.spec.ts @@ -34,6 +34,17 @@ test.describe('Planner: add subtask from detail panel', () => { const panel = page.locator(DETAIL_PANEL); await expect(panel).toBeVisible({ timeout: 5000 }); + // Let the panel's deferred open-time auto-focus land first. It fires ~200ms + // after open (delay(50) + a 150ms guarded focus) and moves focus to the + // first detail item; if it lands *after* we open the inline draft below it + // steals focus from the input, blur-closing it — the input then vanishes + // mid-test and `toBeFocused()` fails with "element(s) not found". + await expect + .poll(async () => + page.evaluate(() => !!document.activeElement?.closest('task-detail-panel')), + ) + .toBe(true); + // The sub-task section starts collapsed; expand it to reveal the button. await panel .locator('mat-expansion-panel-header') diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5a829c9912..92d2f42969 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -59,6 +59,7 @@ import { TaskService } from './features/tasks/task.service'; import { MatMenuItem } from '@angular/material/menu'; import { MatIcon } from '@angular/material/icon'; import { NoteStartupBannerService } from './features/note/note-startup-banner.service'; +import { SyncSafetyBannerService } from './imex/sync/sync-safety-banner.service'; import { ProjectService } from './features/project/project.service'; import { TagService } from './features/tag/tag.service'; import { ContextMenuComponent } from './ui/context-menu/context-menu.component'; @@ -154,6 +155,7 @@ export class AppComponent implements OnDestroy, AfterViewInit { private _tagService = inject(TagService); private _destroyRef = inject(DestroyRef); private _noteStartupBannerService = inject(NoteStartupBannerService); + private _syncSafetyBannerService = inject(SyncSafetyBannerService); private _ngZone = inject(NgZone); private _document = inject(DOCUMENT, { optional: true }); private _startupService = inject(StartupService); @@ -309,6 +311,7 @@ export class AppComponent implements OnDestroy, AfterViewInit { .pipe(take(1)) .subscribe(() => { void this._noteStartupBannerService.showLastNoteIfNeeded(); + this._syncSafetyBannerService.showReminderIfNeeded(); }); // ! For keyboard shortcuts to work correctly with any layouts (QWERTZ/AZERTY/etc) - user's keyboard layout must be presaved diff --git a/src/app/core/banner/banner.model.ts b/src/app/core/banner/banner.model.ts index 57371b238f..99d0ee959d 100644 --- a/src/app/core/banner/banner.model.ts +++ b/src/app/core/banner/banner.model.ts @@ -13,6 +13,7 @@ export enum BannerId { FocusModeSessionDone = 'FocusModeSessionDone', StartupNote = 'StartupNote', DeadlinesToday = 'DeadlinesToday', + SyncSafetyReminder = 'SyncSafetyReminder', } export const BANNER_SORT_PRIO_MAP = { @@ -28,6 +29,7 @@ export const BANNER_SORT_PRIO_MAP = { [BannerId.StartupNote]: 2, [BannerId.Offline]: 0, [BannerId.InstallWebApp]: 0, + [BannerId.SyncSafetyReminder]: 0, } as const; export interface BannerAction { diff --git a/src/app/core/persistence/storage-keys.const.ts b/src/app/core/persistence/storage-keys.const.ts index 019e402109..8c17bbb8fc 100644 --- a/src/app/core/persistence/storage-keys.const.ts +++ b/src/app/core/persistence/storage-keys.const.ts @@ -12,6 +12,11 @@ export enum DB { export enum LS { APP_START_COUNT = 'APP_START_COUNT', APP_START_COUNT_LAST_START_DAY = 'APP_START_COUNT_LAST_START_DAY', + // Epoch ms first observed by SyncSafetyBannerService (seeded once, lazily, on + // its first run). Used only to tell "used for a while" by wall-clock time for + // the sync-setup nudge. NOT a true install date: for installs that predate + // this feature it is seeded at upgrade time, so don't reuse it as one. + SYNC_SAFETY_FIRST_SEEN = 'SUP_SYNC_SAFETY_FIRST_SEEN', RATE_DIALOG_STATE = 'SUP_RATE_DIALOG_STATE', // Set on an unhandled error or any detected data damage; read by the rating // prompt to hold off for a cooldown after a bad experience. Time only. @@ -33,6 +38,10 @@ export enum LS { LAST_NOTE_BANNER_DAY = 'SUP_LAST_NOTE_BANNER_DAY', + // Set once the user acts on or dismisses the "set up sync to keep your data + // safe" startup banner, so the nudge is shown at most once ever. + SYNC_SAFETY_NUDGE_DISMISSED = 'SUP_SYNC_SAFETY_NUDGE_DISMISSED', + SELECTED_TIME_VIEW = 'SELECTED_TIME_VIEW', SCHEDULE_WEEK_ROW_HEIGHT = 'SUP_SCHEDULE_WEEK_ROW_HEIGHT', diff --git a/src/app/imex/sync/sync-safety-banner.service.spec.ts b/src/app/imex/sync/sync-safety-banner.service.spec.ts new file mode 100644 index 0000000000..cf157036b3 --- /dev/null +++ b/src/app/imex/sync/sync-safety-banner.service.spec.ts @@ -0,0 +1,169 @@ +import { TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { SyncSafetyBannerService } from './sync-safety-banner.service'; +import { BannerService } from '../../core/banner/banner.service'; +import { BannerId, Banner } from '../../core/banner/banner.model'; +import { LS } from '../../core/persistence/storage-keys.const'; +import { GlobalConfigService } from '../../features/config/global-config.service'; +import { OnboardingHintService } from '../../features/onboarding/onboarding-hint.service'; +import { SyncConfig } from '../../features/config/global-config.model'; +import { SyncProviderId } from '../../op-log/sync-providers/provider.const'; +import { selectTaskFeatureState } from '../../features/tasks/store/task.selectors'; + +const DAY_MS = 24 * 60 * 60 * 1000; +const NOW = 1_700_000_000_000; +const UNCONFIGURED: Partial = { isEnabled: false, syncProvider: null }; +const CONFIGURED: Partial = { + isEnabled: true, + syncProvider: SyncProviderId.Dropbox, +}; + +describe('SyncSafetyBannerService', () => { + let service: SyncSafetyBannerService; + let bannerService: jasmine.SpyObj; + let matDialog: jasmine.SpyObj; + let syncSpy: jasmine.Spy; + let selectSignalSpy: jasmine.Spy; + let lsStore: Record; + let taskIds: string[]; + + const lastBanner = (): Banner => + bannerService.open.calls.mostRecent().args[0] as Banner; + + const daysAgoTs = (n: number): string => { + const offset = n * DAY_MS; + return (NOW - offset).toString(); + }; + const manyTasks = (): string[] => Array.from({ length: 30 }, (_, i) => `t${i}`); + + // Defaults to a clearly-eligible user: first used 8 days ago, 30 tasks. + const setEligible = (): void => { + lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(8); + taskIds = manyTasks(); + }; + + beforeEach(() => { + lsStore = {}; + taskIds = []; + spyOn(Date, 'now').and.returnValue(NOW); + spyOn(localStorage, 'getItem').and.callFake((k) => lsStore[k] ?? null); + spyOn(localStorage, 'setItem').and.callFake((k, v) => { + lsStore[k] = v; + }); + spyOn(OnboardingHintService, 'isOnboardingInProgress').and.returnValue(false); + + bannerService = jasmine.createSpyObj('BannerService', ['open']); + matDialog = jasmine.createSpyObj('MatDialog', ['open']); + syncSpy = jasmine.createSpy('sync').and.returnValue(UNCONFIGURED); + // Reads live `taskIds` so each test can vary the count before invoking. + selectSignalSpy = jasmine + .createSpy('selectSignal') + .and.callFake(() => () => ({ ids: taskIds })); + + TestBed.configureTestingModule({ + providers: [ + SyncSafetyBannerService, + { provide: BannerService, useValue: bannerService }, + { provide: MatDialog, useValue: matDialog }, + { provide: GlobalConfigService, useValue: { sync: syncSpy } }, + { provide: Store, useValue: { selectSignal: selectSignalSpy } }, + ], + }); + service = TestBed.inject(SyncSafetyBannerService); + }); + + it('shows the banner once used for a while with real data and no sync', () => { + setEligible(); + service.showReminderIfNeeded(); + expect(bannerService.open).toHaveBeenCalledTimes(1); + expect(lastBanner().id).toBe(BannerId.SyncSafetyReminder); + // Guards against accidentally swapping in a different task selector. + expect(selectSignalSpy).toHaveBeenCalledWith(selectTaskFeatureState); + }); + + it('does not show while sync config is still unhydrated (undefined)', () => { + setEligible(); + syncSpy.and.returnValue(undefined); + service.showReminderIfNeeded(); + expect(bannerService.open).not.toHaveBeenCalled(); + // ...but the first-use clock is still seeded for next time. + expect(lsStore[LS.SYNC_SAFETY_FIRST_SEEN]).toBe(daysAgoTs(8)); + }); + + it('shows exactly at the thresholds (7 days, 20 tasks)', () => { + lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(7); + taskIds = Array.from({ length: 20 }, (_, i) => `t${i}`); + service.showReminderIfNeeded(); + expect(bannerService.open).toHaveBeenCalledTimes(1); + }); + + it('seeds the first-use timestamp on first run and does not show yet', () => { + taskIds = manyTasks(); + service.showReminderIfNeeded(); + expect(lsStore[LS.SYNC_SAFETY_FIRST_SEEN]).toBe(NOW.toString()); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('still seeds the first-use clock during onboarding (without showing)', () => { + (OnboardingHintService.isOnboardingInProgress as jasmine.Spy).and.returnValue(true); + taskIds = manyTasks(); + service.showReminderIfNeeded(); + expect(lsStore[LS.SYNC_SAFETY_FIRST_SEEN]).toBe(NOW.toString()); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('does not show before the time threshold even with lots of data', () => { + lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(3); + taskIds = manyTasks(); + service.showReminderIfNeeded(); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('does not show with too little data even after the time threshold', () => { + lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(8); + taskIds = ['a', 'b', 'c', 'd']; // example-task-sized + service.showReminderIfNeeded(); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('does not show again once dismissed', () => { + setEligible(); + lsStore[LS.SYNC_SAFETY_NUDGE_DISMISSED] = 'true'; + service.showReminderIfNeeded(); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('does not interrupt onboarding', () => { + setEligible(); + (OnboardingHintService.isOnboardingInProgress as jasmine.Spy).and.returnValue(true); + service.showReminderIfNeeded(); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('does not show when sync is already configured', () => { + setEligible(); + syncSpy.and.returnValue(CONFIGURED); + service.showReminderIfNeeded(); + expect(bannerService.open).not.toHaveBeenCalled(); + }); + + it('persists dismissal when the user clicks "Not now"', () => { + setEligible(); + service.showReminderIfNeeded(); + lastBanner().action2!.fn(); + expect(lsStore[LS.SYNC_SAFETY_NUDGE_DISMISSED]).toBe('true'); + }); + + it('persists dismissal and opens the sync dialog on "Set up sync"', () => { + const openDialogSpy = spyOn( + service as unknown as { _openSyncCfgDialog: () => Promise }, + '_openSyncCfgDialog', + ).and.resolveTo(); + setEligible(); + service.showReminderIfNeeded(); + lastBanner().action!.fn(); + expect(lsStore[LS.SYNC_SAFETY_NUDGE_DISMISSED]).toBe('true'); + expect(openDialogSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/imex/sync/sync-safety-banner.service.ts b/src/app/imex/sync/sync-safety-banner.service.ts new file mode 100644 index 0000000000..8e5844592a --- /dev/null +++ b/src/app/imex/sync/sync-safety-banner.service.ts @@ -0,0 +1,108 @@ +import { inject, Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { BannerService } from '../../core/banner/banner.service'; +import { BannerId } from '../../core/banner/banner.model'; +import { LS } from '../../core/persistence/storage-keys.const'; +import { GlobalConfigService } from '../../features/config/global-config.service'; +import { OnboardingHintService } from '../../features/onboarding/onboarding-hint.service'; +import { selectTaskFeatureState } from '../../features/tasks/store/task.selectors'; +import { devError } from '../../util/dev-error'; +import { T } from '../../t.const'; + +const DAY_MS = 24 * 60 * 60 * 1000; + +// "Used for a while": wall-clock days since the app was first opened on this +// device. Wall-clock (not app-start count) so it is robust both to users who +// restart many times a day and to those who leave the app running for weeks. +const MIN_DAYS_SINCE_FIRST_USE = 7; + +// ...and there must be a non-trivial amount of data actually worth protecting: +// a fresh install seeds only 4 example tasks, so this keeps us from nudging an +// empty/dormant app even after the time threshold passes. +const MIN_TASKS = 20; + +/** + * Offline-first means a user who never configures sync has no backup at all. + * Once they have clearly used the app for a while AND accumulated real data, + * gently (and exactly once) encourage setting up sync so their data stays safe. + * Mirrors the calm, gated-once pattern of NoteStartupBannerService. + */ +@Injectable({ providedIn: 'root' }) +export class SyncSafetyBannerService { + private readonly _bannerService = inject(BannerService); + private readonly _globalConfigService = inject(GlobalConfigService); + private readonly _matDialog = inject(MatDialog); + private readonly _store = inject(Store); + + private readonly _taskState = this._store.selectSignal(selectTaskFeatureState); + + showReminderIfNeeded(): void { + // Once-ever guard: already acted on / dismissed. + if (localStorage.getItem(LS.SYNC_SAFETY_NUDGE_DISMISSED)) { + return; + } + // Start (or read) the first-use clock. Seeding here means it begins ticking + // the first time the app boots on this device; an install that already + // exists when this feature ships starts its clock now (a deliberate grace + // period rather than nudging every existing user at once). + const firstUseMs = this._getOrSeedFirstUse(); + + // Don't interrupt the first-run onboarding flow. + if (OnboardingHintService.isOnboardingInProgress()) { + return; + } + // Already using sync → nothing to nudge. Treat "config not loaded yet" + // (undefined) as a skip too, so a synced user is never nudged in a race. + const sync = this._globalConfigService.sync(); + if (!sync || (sync.isEnabled && sync.syncProvider)) { + return; + } + // Used for a while? + if (Date.now() - firstUseMs < MIN_DAYS_SINCE_FIRST_USE * DAY_MS) { + return; + } + // Enough data to be worth protecting? + if ((this._taskState().ids?.length ?? 0) < MIN_TASKS) { + return; + } + + this._bannerService.open({ + id: BannerId.SyncSafetyReminder, + msg: T.APP.B_SYNC_SAFETY.MSG, + ico: 'cloud_off', + action: { + label: T.APP.B_SYNC_SAFETY.SETUP, + fn: () => { + this._dismissForever(); + void this._openSyncCfgDialog().catch(devError); + }, + }, + action2: { + label: T.APP.B_SYNC_SAFETY.DISMISS, + fn: () => this._dismissForever(), + }, + isHideDismissBtn: true, + }); + } + + private _getOrSeedFirstUse(): number { + const stored = +(localStorage.getItem(LS.SYNC_SAFETY_FIRST_SEEN) || 0); + if (stored) { + return stored; + } + const now = Date.now(); + localStorage.setItem(LS.SYNC_SAFETY_FIRST_SEEN, now.toString()); + return now; + } + + private _dismissForever(): void { + localStorage.setItem(LS.SYNC_SAFETY_NUDGE_DISMISSED, 'true'); + } + + private async _openSyncCfgDialog(): Promise { + const { DialogSyncCfgComponent } = + await import('./dialog-sync-cfg/dialog-sync-cfg.component'); + this._matDialog.open(DialogSyncCfgComponent); + } +} diff --git a/src/app/t.const.ts b/src/app/t.const.ts index 25b22bde2b..aac4f15cbd 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -6,6 +6,11 @@ const T = { MSG: 'APP.B_INSTALL.MSG', }, B_OFFLINE: 'APP.B_OFFLINE', + B_SYNC_SAFETY: { + DISMISS: 'APP.B_SYNC_SAFETY.DISMISS', + MSG: 'APP.B_SYNC_SAFETY.MSG', + SETUP: 'APP.B_SYNC_SAFETY.SETUP', + }, CONTEXT_MENU: { CHANGE_PROJECT_SETTINGS: 'APP.CONTEXT_MENU.CHANGE_PROJECT_SETTINGS', CHANGE_TAG_SETTINGS: 'APP.CONTEXT_MENU.CHANGE_TAG_SETTINGS', diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json index 41bd6fb3a0..519309123a 100644 --- a/src/assets/i18n/ar.json +++ b/src/assets/i18n/ar.json @@ -6,6 +6,11 @@ "MSG": "هل تريد تثبيت الإنتاجية الفائقة كتطبيق ويب؟" }, "B_OFFLINE": "أنت غير متصل بالإنترنت. لن تعمل مزامنة بيانات موفر المشكلة وطلبها.", + "B_SYNC_SAFETY": { + "DISMISS": "ليس الآن", + "MSG": "أنت تستخدم Super Productivity منذ فترة. قم بإعداد المزامنة لعمل نسخة احتياطية من بياناتك والحفاظ عليها آمنة عبر جميع أجهزتك.", + "SETUP": "إعداد المزامنة" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "تغيير الخلفية", "CHANGE_PROJECT_SETTINGS": "تغيير إعدادات المشروع", diff --git a/src/assets/i18n/cs.json b/src/assets/i18n/cs.json index e006aa5fdb..dc4f7aa9f7 100644 --- a/src/assets/i18n/cs.json +++ b/src/assets/i18n/cs.json @@ -6,6 +6,11 @@ "MSG": "Chcete nainstalovat Super Produktivitu jako PWA?" }, "B_OFFLINE": "Jste odpojeni od internetu. Synchronizace a požadavky na data poskytovatele problémů nebudou fungovat.", + "B_SYNC_SAFETY": { + "DISMISS": "Teď ne", + "MSG": "Super Productivity používáte už nějakou dobu. Nastavte synchronizaci, abyste zálohovali svá data a udrželi je v bezpečí na všech zařízeních.", + "SETUP": "Nastavit synchronizaci" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Změnit pozadí" }, diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index b886b26595..da291f0023 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -6,6 +6,11 @@ "MSG": "Möchtest du Super Productivity als PWA installieren?" }, "B_OFFLINE": "Du bist vom Internet getrennt. Das Synchronisieren und Anfordern von Daten des Issue-Providers funktioniert nicht.", + "B_SYNC_SAFETY": { + "DISMISS": "Jetzt nicht", + "MSG": "Du nutzt Super Productivity schon eine Weile. Richte die Synchronisierung ein, um deine Daten zu sichern und geräteübergreifend zu schützen.", + "SETUP": "Synchronisierung einrichten" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Projekteinstellungen ändern", "CHANGE_TAG_SETTINGS": "Tag-Einstellungen ändern" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 0e43d10151..218c24164d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -6,6 +6,11 @@ "MSG": "Do you want to install Super Productivity as a PWA (Progressive Web App)?" }, "B_OFFLINE": "You are disconnected from the internet. Syncing and requesting issue provider data will not work.", + "B_SYNC_SAFETY": { + "DISMISS": "Not now", + "MSG": "You've been using Super Productivity for a while. Set up sync to back up your data and keep it safe across devices.", + "SETUP": "Set up sync" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Change Project Settings", "CHANGE_TAG_SETTINGS": "Change Tag Settings" diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 41f631cf02..f761717bb0 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -6,6 +6,11 @@ "MSG": "¿Quieres instalar Super Productivity como una PWA?" }, "B_OFFLINE": "Estás desconectado de internet. La sincronización y la solicitud de datos al proveedor de incidencias no funcionarán.", + "B_SYNC_SAFETY": { + "DISMISS": "Ahora no", + "MSG": "Llevas un tiempo usando Super Productivity. Configura la sincronización para hacer una copia de seguridad de tus datos y mantenerlos seguros en todos tus dispositivos.", + "SETUP": "Configurar sincronización" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Cambiar fondo", "CHANGE_PROJECT_SETTINGS": "Cambiar configuración del proyecto", diff --git a/src/assets/i18n/fa.json b/src/assets/i18n/fa.json index 378e7803c5..9b097efeef 100644 --- a/src/assets/i18n/fa.json +++ b/src/assets/i18n/fa.json @@ -6,6 +6,11 @@ "MSG": "آیا میخواهید سوپر پروداکتیویتی به عنوان PWA نصب بشود؟" }, "B_OFFLINE": "شما از اینترنت جدا هستید. همگام سازی و درخواست داده های ارائه دهنده مشکل کار نمی کند.", + "B_SYNC_SAFETY": { + "DISMISS": "اکنون نه", + "MSG": "مدتی است که از Super Productivity استفاده می‌کنید. همگام‌سازی را تنظیم کنید تا از داده‌های خود نسخه پشتیبان تهیه کرده و آن‌ها را در همه دستگاه‌ها ایمن نگه دارید.", + "SETUP": "تنظیم همگام‌سازی" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "تغییر پیش زمینه" }, diff --git a/src/assets/i18n/fi.json b/src/assets/i18n/fi.json index aa925a1f7e..d77d8f6f01 100644 --- a/src/assets/i18n/fi.json +++ b/src/assets/i18n/fi.json @@ -6,6 +6,11 @@ "MSG": "Haluatko asentaa Super Productivityn PWA-sovellukseksi?" }, "B_OFFLINE": "Olet irti internetistä. Synkronointi ja tiedustelut ongelmanantajien tiedoista eivät toimi.", + "B_SYNC_SAFETY": { + "DISMISS": "Ei nyt", + "MSG": "Olet käyttänyt Super Productivitya jo jonkin aikaa. Määritä synkronointi varmuuskopioidaksesi tietosi ja pitääksesi ne turvassa kaikilla laitteilla.", + "SETUP": "Määritä synkronointi" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Vaihda taustakuva" }, diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index baf77891b9..062bf7180c 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -6,6 +6,11 @@ "MSG": "Voulez-vous installer Super Productivity en tant que PWA ?" }, "B_OFFLINE": "Vous êtes déconnecté d'Internet. La synchronisation et la demande des données du fournisseur de problèmes ne fonctionneront pas.", + "B_SYNC_SAFETY": { + "DISMISS": "Pas maintenant", + "MSG": "Vous utilisez Super Productivity depuis un certain temps. Configurez la synchronisation pour sauvegarder vos données et les garder en sécurité sur tous vos appareils.", + "SETUP": "Configurer la synchronisation" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Changer le fond", "CHANGE_PROJECT_SETTINGS": "Modifier les paramètres du projet", diff --git a/src/assets/i18n/hr.json b/src/assets/i18n/hr.json index 9beb683f2f..4c9165f0a3 100644 --- a/src/assets/i18n/hr.json +++ b/src/assets/i18n/hr.json @@ -6,6 +6,11 @@ "MSG": "Želiš instalirati Super Productivity kao PWA (Progressive Web App)?" }, "B_OFFLINE": "Nemaš vezu s internetom. Sinkronizacija i dohvaćanje podataka od usluge za praćenje problema neće raditi.", + "B_SYNC_SAFETY": { + "DISMISS": "Ne sada", + "MSG": "Već neko vrijeme koristiš Super Productivity. Postavi sinkronizaciju kako bi sigurnosno kopirao svoje podatke i čuvao ih sigurnima na svim uređajima.", + "SETUP": "Postavi sinkronizaciju" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Promijeni postavke projekta", "CHANGE_TAG_SETTINGS": "Promijeni postavke oznaka" diff --git a/src/assets/i18n/id.json b/src/assets/i18n/id.json index 744e8f548e..e58185be80 100644 --- a/src/assets/i18n/id.json +++ b/src/assets/i18n/id.json @@ -6,6 +6,11 @@ "MSG": "Apa kamu ingin memasang Super Productivity sebagai sebuah PWA?" }, "B_OFFLINE": "Jaringanmu terputus dari internet. Sinkronasi dan meminta masalah penyedia data mungkin tidak akan bekerja.", + "B_SYNC_SAFETY": { + "DISMISS": "Nanti saja", + "MSG": "Kamu sudah menggunakan Super Productivity beberapa waktu. Siapkan sinkronisasi untuk mencadangkan datamu dan menjaganya tetap aman di semua perangkat.", + "SETUP": "Siapkan sinkronisasi" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Ubah Latar Belakang", "CHANGE_PROJECT_SETTINGS": "Ubah pengaturan proyek", diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 3f4eb11999..c6d8abffd0 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -6,6 +6,11 @@ "MSG": "Vuoi installare Super Productivity come PWA?" }, "B_OFFLINE": "Sei disconnesso da Internet. La sincronizzazione e la richiesta dei dati del provider di problemi non funzioneranno.", + "B_SYNC_SAFETY": { + "DISMISS": "Non ora", + "MSG": "Usi Super Productivity da un po'. Configura la sincronizzazione per eseguire il backup dei tuoi dati e tenerli al sicuro su tutti i dispositivi.", + "SETUP": "Configura sincronizzazione" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Cambia sfondo", "CHANGE_PROJECT_SETTINGS": "Modifica impostazioni progetto", diff --git a/src/assets/i18n/ja.json b/src/assets/i18n/ja.json index 83519a8696..c8eec8c234 100644 --- a/src/assets/i18n/ja.json +++ b/src/assets/i18n/ja.json @@ -6,6 +6,11 @@ "MSG": "Super ProductivityをPWAとしてインストールしますか?" }, "B_OFFLINE": "インターネットに接続していません。課題プロバイダーデータの同期と要求は機能しません。", + "B_SYNC_SAFETY": { + "DISMISS": "後で", + "MSG": "Super Productivity をしばらくご利用いただいています。同期を設定して、データをバックアップし、すべてのデバイスで安全に保ちましょう。", + "SETUP": "同期を設定" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "変更の背景", "CHANGE_PROJECT_SETTINGS": "プロジェクト設定を変更", diff --git a/src/assets/i18n/ko.json b/src/assets/i18n/ko.json index 52b83cc5b2..2b0490a1fa 100644 --- a/src/assets/i18n/ko.json +++ b/src/assets/i18n/ko.json @@ -6,6 +6,11 @@ "MSG": "PWA로 Super Productivity를 설치하시겠습니까?" }, "B_OFFLINE": "인터넷 연결이 끊어졌습니다. 문제 제공자 데이터 동기화 및 요청이 작동하지 않습니다.", + "B_SYNC_SAFETY": { + "DISMISS": "나중에", + "MSG": "Super Productivity를 사용하신 지 꽤 되셨네요. 동기화를 설정하여 데이터를 백업하고 모든 기기에서 안전하게 보관하세요.", + "SETUP": "동기화 설정" + }, "UPDATE_WEB_APP": "새 버전을 사용할 수 있습니다. 새 버전을 로드하시겠습니까?", "CONTEXT_MENU": { "CHANGE_BACKGROUND": "배경 변경", diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index 4162fce3a0..5bb23316bc 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -6,6 +6,11 @@ "MSG": "Vil du installere Super Productivity som en PWA?" }, "B_OFFLINE": "Du er koblet fra internett. Synkronisering og å be om data fra leverandører fungerer ikke.", + "B_SYNC_SAFETY": { + "DISMISS": "Ikke nå", + "MSG": "Du har brukt Super Productivity en stund. Sett opp synkronisering for å sikkerhetskopiere dataene dine og holde dem trygge på tvers av enheter.", + "SETUP": "Sett opp synkronisering" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Endre bakgrunn" }, diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index ec76924b63..6301ce6ab9 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -6,6 +6,11 @@ "MSG": "Wilt u Super Productivity als PWA installeren?" }, "B_OFFLINE": "Je hebt geen verbinding met internet. Het synchroniseren en opvragen van probleemprovidergegevens werkt niet.", + "B_SYNC_SAFETY": { + "DISMISS": "Niet nu", + "MSG": "Je gebruikt Super Productivity al een tijdje. Stel synchronisatie in om een back-up van je gegevens te maken en ze veilig te houden op al je apparaten.", + "SETUP": "Synchronisatie instellen" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Achtergrond wijzigen", "CHANGE_PROJECT_SETTINGS": "Projectinstellingen wijzigen", diff --git a/src/assets/i18n/pl.json b/src/assets/i18n/pl.json index ff691502c4..438ed05162 100644 --- a/src/assets/i18n/pl.json +++ b/src/assets/i18n/pl.json @@ -6,6 +6,11 @@ "MSG": "Czy chcesz zinstalować Super Productivity jako aplikację PWA?" }, "B_OFFLINE": "Zostałeś odłączony od internetu. Synchronizacja nie będzie działać.", + "B_SYNC_SAFETY": { + "DISMISS": "Nie teraz", + "MSG": "Używasz Super Productivity już od jakiegoś czasu. Skonfiguruj synchronizację, aby utworzyć kopię zapasową danych i zapewnić im bezpieczeństwo na wszystkich urządzeniach.", + "SETUP": "Skonfiguruj synchronizację" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Zmień ustawienia Projektu", "CHANGE_TAG_SETTINGS": "Zmień ustawienia Tagów" diff --git a/src/assets/i18n/pt-br.json b/src/assets/i18n/pt-br.json index d120bb4490..b916b385f2 100644 --- a/src/assets/i18n/pt-br.json +++ b/src/assets/i18n/pt-br.json @@ -6,6 +6,11 @@ "MSG": "Deseja instalar o Super Productivity como um PWA (Progressive Web App)?" }, "B_OFFLINE": "Você está desconectado da internet. A sincronização e a solicitação de dados de provedores de issues não funcionarão.", + "B_SYNC_SAFETY": { + "DISMISS": "Agora não", + "MSG": "Você usa o Super Productivity há algum tempo. Configure a sincronização para fazer backup dos seus dados e mantê-los seguros em todos os dispositivos.", + "SETUP": "Configurar sincronização" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Alterar Configurações do Projeto", "CHANGE_TAG_SETTINGS": "Alterar Configurações da Etiqueta" diff --git a/src/assets/i18n/pt.json b/src/assets/i18n/pt.json index 9c6e657e3f..5bd81742eb 100644 --- a/src/assets/i18n/pt.json +++ b/src/assets/i18n/pt.json @@ -6,6 +6,11 @@ "MSG": "Deseja instalar Super Productivity como PWA?" }, "B_OFFLINE": "Você está desconectado da internet. A sincronização e solicitação de dados do provedor de problemas não funcionará.", + "B_SYNC_SAFETY": { + "DISMISS": "Agora não", + "MSG": "Já usa o Super Productivity há algum tempo. Configure a sincronização para fazer uma cópia de segurança dos seus dados e mantê-los seguros em todos os dispositivos.", + "SETUP": "Configurar sincronização" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Mudar Fundo", "CHANGE_PROJECT_SETTINGS": "Alterar definições do projeto", diff --git a/src/assets/i18n/ro-md.json b/src/assets/i18n/ro-md.json index d56eb93412..39955baf10 100644 --- a/src/assets/i18n/ro-md.json +++ b/src/assets/i18n/ro-md.json @@ -6,6 +6,11 @@ "MSG": "Vrei să instalezi Super Productivity ca PWA (Progressive Web App)?" }, "B_OFFLINE": "Ești deconectat de la internet. Sincronizarea și solicitarea datelor furnizorului de probleme nu vor funcționa.", + "B_SYNC_SAFETY": { + "DISMISS": "Nu acum", + "MSG": "Folosești Super Productivity de ceva timp. Configurează sincronizarea pentru a face o copie de rezervă a datelor tale și a le păstra în siguranță pe toate dispozitivele.", + "SETUP": "Configurează sincronizarea" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Schimbă setările proiectului", "CHANGE_TAG_SETTINGS": "Schimbă setările etichetelor" diff --git a/src/assets/i18n/ro.json b/src/assets/i18n/ro.json index c509f3e592..b40bb298f5 100644 --- a/src/assets/i18n/ro.json +++ b/src/assets/i18n/ro.json @@ -6,6 +6,11 @@ "MSG": "Vrei să instalezi Super Productivity ca PWA (Progressive Web App)?" }, "B_OFFLINE": "Ești deconectat de la internet. Sincronizarea și solicitarea datelor furnizorului de probleme nu vor funcționa.", + "B_SYNC_SAFETY": { + "DISMISS": "Nu acum", + "MSG": "Folosești Super Productivity de ceva timp. Configurează sincronizarea pentru a face o copie de rezervă a datelor tale și a le păstra în siguranță pe toate dispozitivele.", + "SETUP": "Configurează sincronizarea" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Schimbă setările proiectului", "CHANGE_TAG_SETTINGS": "Schimbă setările etichetelor" diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 36f31fa555..0a80378a09 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -6,6 +6,11 @@ "MSG": "Хотите установить Super Productivity в качестве PWA?" }, "B_OFFLINE": "Вы отключены от интернета. Синхронизация и запрос данных поставщика проблемы не будут работать.", + "B_SYNC_SAFETY": { + "DISMISS": "Не сейчас", + "MSG": "Вы пользуетесь Super Productivity уже некоторое время. Настройте синхронизацию, чтобы создать резервную копию данных и сохранить их в безопасности на всех устройствах.", + "SETUP": "Настроить синхронизацию" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Изменить фон", "CHANGE_PROJECT_SETTINGS": "Изменить настройки проекта", diff --git a/src/assets/i18n/sk.json b/src/assets/i18n/sk.json index f36ccb625b..3c65db1896 100644 --- a/src/assets/i18n/sk.json +++ b/src/assets/i18n/sk.json @@ -6,6 +6,11 @@ "MSG": "Chcete nainštalovať aplikáciu Super Productivity ako PWA?" }, "B_OFFLINE": "Boli ste odpojení od internetu. Synchronizácia a vyžiadanie údajov nebudú fungovať.", + "B_SYNC_SAFETY": { + "DISMISS": "Teraz nie", + "MSG": "Super Productivity používate už nejaký čas. Nastavte synchronizáciu, aby ste zálohovali svoje údaje a udržali ich v bezpečí na všetkých zariadeniach.", + "SETUP": "Nastaviť synchronizáciu" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Zmeniť pozadie" }, diff --git a/src/assets/i18n/sv.json b/src/assets/i18n/sv.json index 90f0f8f956..92770f4097 100644 --- a/src/assets/i18n/sv.json +++ b/src/assets/i18n/sv.json @@ -6,6 +6,11 @@ "MSG": "Vill du installera Super Productivity som en PWA?" }, "B_OFFLINE": "Du är bortkopplad från internet. Synkronisering och begäran om data från ärendeleverantören fungerar inte.", + "B_SYNC_SAFETY": { + "DISMISS": "Inte nu", + "MSG": "Du har använt Super Productivity ett tag. Konfigurera synkronisering för att säkerhetskopiera dina data och hålla dem säkra på alla enheter.", + "SETUP": "Konfigurera synkronisering" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "Ändra bakgrund" }, diff --git a/src/assets/i18n/tr.json b/src/assets/i18n/tr.json index bc33922132..09335916c8 100644 --- a/src/assets/i18n/tr.json +++ b/src/assets/i18n/tr.json @@ -6,6 +6,11 @@ "MSG": "Süper Verimliliği PWA olarak kurmak ister misiniz?" }, "B_OFFLINE": "İnternet bağlantınız kesildi. Verileri senkronize etmek ve sağlayıcıdan sorunları istemek işe yaramaz.", + "B_SYNC_SAFETY": { + "DISMISS": "Şimdi değil", + "MSG": "Super Productivity'yi bir süredir kullanıyorsunuz. Verilerinizi yedeklemek ve tüm cihazlarınızda güvende tutmak için senkronizasyonu ayarlayın.", + "SETUP": "Senkronizasyonu ayarla" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Proje Ayarlarını Değiştir", "CHANGE_TAG_SETTINGS": "Etiket Ayarlarını Değiştir" diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json index f397a0500a..38fd3ef99b 100644 --- a/src/assets/i18n/uk.json +++ b/src/assets/i18n/uk.json @@ -6,6 +6,11 @@ "MSG": "Бажаєте встановити Super Productivity як PWA?" }, "B_OFFLINE": "Ви не підключені до інтернету. Синхронізація та запит даних з провайдера завдань не працюватимуть.", + "B_SYNC_SAFETY": { + "DISMISS": "Не зараз", + "MSG": "Ви користуєтеся Super Productivity вже певний час. Налаштуйте синхронізацію, щоб створити резервну копію даних і зберегти їх у безпеці на всіх пристроях.", + "SETUP": "Налаштувати синхронізацію" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Змінити налаштування проекту", "CHANGE_TAG_SETTINGS": "Змінити налаштування тегів" diff --git a/src/assets/i18n/vi.json b/src/assets/i18n/vi.json index e5b0a61ea0..b10803d5f4 100644 --- a/src/assets/i18n/vi.json +++ b/src/assets/i18n/vi.json @@ -6,6 +6,11 @@ "MSG": "Bạn có muốn cài đặt Super Productivity dưới dạng PWA (Ứng dụng web tiến bộ) không?" }, "B_OFFLINE": "Bạn đang ngoại tuyến. Đồng bộ và yêu cầu dữ liệu từ nhà cung cấp sự cố sẽ không hoạt động.", + "B_SYNC_SAFETY": { + "DISMISS": "Để sau", + "MSG": "Bạn đã sử dụng Super Productivity một thời gian. Hãy thiết lập đồng bộ hóa để sao lưu dữ liệu của bạn và giữ an toàn trên mọi thiết bị.", + "SETUP": "Thiết lập đồng bộ hóa" + }, "CONTEXT_MENU": { "CHANGE_PROJECT_SETTINGS": "Thay đổi cài đặt dự án", "CHANGE_TAG_SETTINGS": "Thay đổi cài đặt nhãn" diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index cc7eabcbeb..4ebc1b1fea 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -6,6 +6,11 @@ "MSG": "您想將 Super Productivity 安裝為 PWA 嗎?" }, "B_OFFLINE": "您已斷開與網際網路的連接。同步和資料請求將失效。", + "B_SYNC_SAFETY": { + "DISMISS": "暫不", + "MSG": "您已經使用 Super Productivity 一段時間了。設定同步以備份您的資料,並在所有裝置上保持安全。", + "SETUP": "設定同步" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "更改背景", "CHANGE_PROJECT_SETTINGS": "變更專案設定", diff --git a/src/assets/i18n/zh.json b/src/assets/i18n/zh.json index 20360b0d37..8aade13836 100644 --- a/src/assets/i18n/zh.json +++ b/src/assets/i18n/zh.json @@ -6,6 +6,11 @@ "MSG": "您想要将超级生产力安装为 PWA 吗?" }, "B_OFFLINE": "您已断开互联网连接。同步和请求问题提供者数据将无法工作。", + "B_SYNC_SAFETY": { + "DISMISS": "暂不", + "MSG": "您已经使用 Super Productivity 一段时间了。设置同步以备份您的数据,并在所有设备上保持安全。", + "SETUP": "设置同步" + }, "CONTEXT_MENU": { "CHANGE_BACKGROUND": "更改背景", "CHANGE_PROJECT_SETTINGS": "更改项目设置",