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)[][][]