diff --git a/src/_locales/dict.setup-page.ts b/src/_locales/dict.setup-page.ts index 080ac0399..b2cf408a0 100644 --- a/src/_locales/dict.setup-page.ts +++ b/src/_locales/dict.setup-page.ts @@ -7840,6 +7840,26 @@ Przykłady: "*", "ctrl+$", "ctrl+alt+g"`, zh_TW: '刪除', ja: 'スナップショットを削除', }, + 'snapshot.btn_make_permanent': { + en: 'Make snapshot permanent', + ru: 'Сделать снепшот постоянным', + }, + 'snapshot.btn_make_temporary': { + en: 'Make snapshot temporary', + ru: 'Сделать снепшот временным', + }, + 'snapshot.confirm_delete_permanent': { + en: 'Are you sure you want to delete this permanent snapshot?', + ru: 'Вы уверены, что хотите удалить этот постоянный снепшот?', + }, + 'snapshot.permanent_snapshots_header': { + en: 'Permanent Snapshots', + ru: 'Постоянные снепшоты', + }, + 'snapshot.temporary_snapshots_header': { + en: 'Temporary Snapshots', + ru: 'Временные снепшоты', + }, 'snapshot.window_title': { en: 'Window', de: 'Fenster', diff --git a/src/bg/background.ts b/src/bg/background.ts index cbc4f1676..126f02336 100644 --- a/src/bg/background.ts +++ b/src/bg/background.ts @@ -43,6 +43,9 @@ void (async function main() { createSnapshot: Snapshots.createSnapshot, addSnapshot: Snapshots.addSnapshot, removeSnapshot: Snapshots.removeSnapshot, + makeSnapshotPermanent: Snapshots.makeSnapshotPermanent, + makeSnapshotTemporary: Snapshots.makeSnapshotTemporary, + renameSnapshot: Snapshots.renameSnapshot, openSnapshotWindows: Snapshots.openWindows, createWindowWithTabs: Windows.createWithTabs, isWindowTabsLocked: Windows.isWindowTabsLocked, diff --git a/src/page.setup/components/snapshots.vue b/src/page.setup/components/snapshots.vue index 17629c13a..2021d7dd6 100644 --- a/src/page.setup/components/snapshots.vue +++ b/src/page.setup/components/snapshots.vue @@ -3,26 +3,55 @@ .wrapper .snapshot-list-section .snapshot-list(:data-empty="!state.snapshots.length") - .controls - .btn(@click="createSnapshot()") {{translate('snapshot.btn_create_snapshot')}} - .btn - .label {{translate('snapshot.btn_import_snapshot')}} - input(type="file" accept="application/json" @input="importSnapshot") - .snapshot( - v-for="snapshot in state.snapshots" - :key="snapshot.id" - :id="String(snapshot.id)" - :data-active="state.activeSnapshot?.id === snapshot.id" - @click="activateSnapshot(snapshot)") - .info - .date-time {{snapshot.dateStr}} - {{snapshot.timeStr}} - .content-info {{getSnapInfo(snapshot)}} - .rm-btn(:title="translate('snapshot.btn_remove')" @click="removeSnapshot(snapshot)") - svg: use(href="#icon_trash") + .permanent-snapshots-section(v-if="permanentSnapshots.length") + .perm-section-header {{translate('snapshot.permanent_snapshots_header')}} + .snapshot( + v-for="snapshot in permanentSnapshots" + :key="snapshot.id" + :id="String(snapshot.id)" + :data-active="state.activeSnapshot?.id === snapshot.id" + :data-permanent="true" + @click="activateSnapshot(snapshot)") + .info + .date-time {{snapshot.title ?? snapshot.dateStr + ' - ' + snapshot.timeStr}} + .content-info {{getSnapInfo(snapshot)}} + .rm-btn(:title="translate('snapshot.btn_remove')" @click.stop="removeSnapshot(snapshot)") + svg: use(href="#icon_trash") + + .temporary-snapshots-section + .controls + .btn(@click="createSnapshot()") {{translate('snapshot.btn_create_snapshot')}} + .btn + .label {{translate('snapshot.btn_import_snapshot')}} + input(type="file" accept="application/json" @input="importSnapshot") + .temp-section-header {{translate('snapshot.temporary_snapshots_header')}} + + .snapshot( + v-for="snapshot in temporarySnapshots" + :key="snapshot.id" + :id="String(snapshot.id)" + :data-active="state.activeSnapshot?.id === snapshot.id" + @click="activateSnapshot(snapshot)") + .info + .date-time {{snapshot.dateStr}} - {{snapshot.timeStr}} + .content-info {{getSnapInfo(snapshot)}} + .rm-btn(:title="translate('snapshot.btn_remove')" @click="removeSnapshot(snapshot)") + svg: use(href="#icon_trash") .active-snapshot-section .header(v-if="state.activeSnapshot" :data-empty="!state.activeSnapshot") - .title(@wheel="onHeaderWheel") {{state.activeSnapshot?.dateStr ?? '?'}} - {{state.activeSnapshot?.timeStr ?? '?'}} + template(v-if="state.activeSnapshot?.permanent") + .title + input.title-input( + ref="titleInputRef" + :value="state.activeSnapshot.title ?? state.activeSnapshot.dateStr + ' - ' + state.activeSnapshot.timeStr" + @change="onRenameActiveSnapshot($event)" + @keydown.enter="($event.target as HTMLInputElement).blur()") + .title(v-else @wheel="onHeaderWheel") {{state.activeSnapshot?.dateStr ?? '?'}} - {{state.activeSnapshot?.timeStr ?? '?'}} + .btn(v-if="state.activeSnapshot?.permanent" @click="doMakeTemporary(state.activeSnapshot)"). + {{translate('snapshot.btn_make_temporary')}} + .btn(v-else @click="doMakePermanent(state.activeSnapshot)"). + {{translate('snapshot.btn_make_permanent')}} DropDownButton( :label="translate('snapshot.btn_export_snapshot')" @open="onExportSnapshotDropDownOpen") @@ -90,7 +119,7 @@ diff --git a/src/services/snapshots.bg.ts b/src/services/snapshots.bg.ts index 22fdc16da..01808b376 100644 --- a/src/services/snapshots.bg.ts +++ b/src/services/snapshots.bg.ts @@ -491,6 +491,7 @@ function limitSnapshots(snapshots: Snapshot[]): Snapshot[] | undefined { while (index--) { const snapshot = snapshots[index] if (!snapshot) continue + if (snapshot.permanent) continue sizeAccum += new Blob([JSON.stringify(snapshot)]).size @@ -508,11 +509,90 @@ function limitSnapshots(snapshots: Snapshot[]): Snapshot[] | undefined { index++ - const normSnapshot = getNormalizedSnapshot(snapshots, index) - if (normSnapshot) snapshots[index] = normSnapshot + if (index === 0) return + + // Find the first non-permanent snapshot at or after the cutoff + let firstNonPermIndex = index + while (firstNonPermIndex < snapshots.length && snapshots[firstNonPermIndex]?.permanent) { + firstNonPermIndex++ + } + if (firstNonPermIndex >= snapshots.length) return + + // Normalize the first kept non-permanent snapshot while the full chain is still intact + const normSnapshot = getNormalizedSnapshot(snapshots, firstNonPermIndex) + if (normSnapshot) snapshots[firstNonPermIndex] = normSnapshot else return - return snapshots.slice(index) + // Keep all permanent snapshots plus all snapshots from firstNonPermIndex onward + const result: Snapshot[] = [] + for (let i = 0; i < snapshots.length; i++) { + const snap = snapshots[i] + if (!snap) continue + if (snap.permanent || i >= firstNonPermIndex) result.push(snap) + } + return result +} + +export async function makeSnapshotPermanent(id: ID): Promise { + let stored + try { + stored = await browser.storage.local.get(['snapshots']) + } catch (err) { + Logs.err('makeSnapshotPermanent: Cannot get snapshots', err) + return RemovingSnapshotResult.Err + } + if (!stored.snapshots) return RemovingSnapshotResult.Err + + const index = stored.snapshots.findIndex(s => s.id === id) + if (index === -1) return RemovingSnapshotResult.Err + + const normSnapshot = getNormalizedSnapshot(stored.snapshots, index) + if (!normSnapshot) return RemovingSnapshotResult.Err + normSnapshot.permanent = true + stored.snapshots[index] = normSnapshot + + await Store.set({ snapshots: stored.snapshots }) + return RemovingSnapshotResult.Ok +} + +export async function makeSnapshotTemporary(id: ID): Promise { + let stored + try { + stored = await browser.storage.local.get(['snapshots']) + } catch (err) { + Logs.err('makeSnapshotTemporary: Cannot get snapshots', err) + return RemovingSnapshotResult.Err + } + if (!stored.snapshots) return RemovingSnapshotResult.Err + + const snapshot = stored.snapshots.find(s => s.id === id) + if (!snapshot) return RemovingSnapshotResult.Err + + delete snapshot.permanent + delete snapshot.title + + await Store.set({ snapshots: stored.snapshots }) + return RemovingSnapshotResult.Ok +} + +export async function renameSnapshot(id: ID, title: string): Promise { + let stored + try { + stored = await browser.storage.local.get(['snapshots']) + } catch (err) { + Logs.err('renameSnapshot: Cannot get snapshots', err) + return RemovingSnapshotResult.Err + } + if (!stored.snapshots) return RemovingSnapshotResult.Err + + const snapshot = stored.snapshots.find(s => s.id === id) + if (!snapshot) return RemovingSnapshotResult.Err + + if (title) snapshot.title = title + else delete snapshot.title + + await Store.set({ snapshots: stored.snapshots }) + return RemovingSnapshotResult.Ok } export async function removeSnapshot(id: ID): Promise { diff --git a/src/services/snapshots.fg.ts b/src/services/snapshots.fg.ts index c1df0c764..e95e163f5 100644 --- a/src/services/snapshots.fg.ts +++ b/src/services/snapshots.fg.ts @@ -10,6 +10,7 @@ import * as Favicons from 'src/services/favicons.fg' import * as Utils from 'src/utils' import * as Windows from 'src/services/windows.fg' import * as Containers from 'src/services/containers.fg' +import { RemovingSnapshotResult } from 'src/enums' import { getNormalizedSnapshot, GLOB_PINNED_ID } from 'src/services/snapshots' export * from 'src/services/snapshots' @@ -268,6 +269,18 @@ export async function addSnapshot(snapshot: T.NormalizedSnapshot): Promise return await IPC.bg('addSnapshot', snapshot) } +export async function makeSnapshotPermanent(id: ID): Promise { + return await IPC.bg('makeSnapshotPermanent', id) +} + +export async function makeSnapshotTemporary(id: ID): Promise { + return await IPC.bg('makeSnapshotTemporary', id) +} + +export async function renameSnapshot(id: ID, title: string): Promise { + return await IPC.bg('renameSnapshot', id, title) +} + export function updateInternalUrls(snapshot: T.NormalizedSnapshot): void { for (const win of snapshot.tabs) { for (const panel of win) { diff --git a/src/styles/page.setup/snapshots.styl b/src/styles/page.setup/snapshots.styl index 0c6a53617..334fc3980 100644 --- a/src/styles/page.setup/snapshots.styl +++ b/src/styles/page.setup/snapshots.styl @@ -131,6 +131,53 @@ height: 100% overflow: hidden +// --- +// -- Permanent snapshots section +// - +.Snapshots .permanent-snapshots-section + padding-bottom: 4px + margin-bottom: 4px + border-bottom: 1px solid var(--border-color) + +.Snapshots .temporary-snapshots-section + padding-top: 4px + +.Snapshots .perm-section-header +.Snapshots .temp-section-header + position: relative + font-size: rem(10) + font-weight: 700 + color: var(--toolbar-fg) + opacity: .45 + padding: 8px 8px 4px + text-transform: uppercase + letter-spacing: .06em + user-select: none + +.Snapshots .snapshot[data-permanent="true"]:before + opacity: .07 + +.Snapshots .title-input + background: none + border: none + border-bottom: 1px solid transparent + outline: none + color: var(--toolbar-fg) + font-size: rem(15) + width: 100% + cursor: default + padding: 0 + font-family: inherit + &:focus + cursor: text + border-bottom-color: var(--toolbar-fg) + +.Snapshots .active-snapshot-section .header .title .title-input + font-size: rem(24) + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + // --- // -- Timeline // - diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 1fdc3462b..ba3ca146c 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -47,6 +47,9 @@ export type BgActions = { createSnapshot: typeof Snapshots.createSnapshot addSnapshot: typeof Snapshots.addSnapshot removeSnapshot: typeof Snapshots.removeSnapshot + makeSnapshotPermanent: typeof Snapshots.makeSnapshotPermanent + makeSnapshotTemporary: typeof Snapshots.makeSnapshotTemporary + renameSnapshot: typeof Snapshots.renameSnapshot openSnapshotWindows: typeof Snapshots.openWindows saveFavicon: typeof Favicons.saveFavicon reloadFavicons: () => any diff --git a/src/types/snapshots.ts b/src/types/snapshots.ts index 7b266d2fd..15c9e65f4 100644 --- a/src/types/snapshots.ts +++ b/src/types/snapshots.ts @@ -5,6 +5,8 @@ import * as E from 'src/enums' export interface Snapshot { id: ID time: number + permanent?: boolean + title?: string containers: Record | E.SnapStoreMode.Unchanged sidebar: SidebarConfig | E.SnapStoreMode.Unchanged tabs: (SnapTab | E.SnapStoreMode.Unchanged)[][][]