Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/_locales/dict.setup-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/bg/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
124 changes: 100 additions & 24 deletions src/page.setup/components/snapshots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -90,7 +119,7 @@
</template>

<script lang="ts" setup>
import { computed, nextTick, reactive } from 'vue'
import { computed, nextTick, reactive, ref } from 'vue'
import type * as T from 'src/types'
import { SnapOpenType, RemovingSnapshotResult } from 'src/enums'
import { CONTAINER_ID, NOID } from 'src/defaults'
Expand Down Expand Up @@ -122,6 +151,15 @@ const state = reactive({
mouseUpShiftMode: true,
} as SnapshotsViewerState)

const titleInputRef = ref<HTMLInputElement | null>(null)

const permanentSnapshots = computed<T.SnapshotState[]>(() =>
state.snapshots.filter(s => s.permanent)
)
const temporarySnapshots = computed<T.SnapshotState[]>(() =>
state.snapshots.filter(s => !s.permanent)
)

const selectedTabsLen = computed<number>(() => {
if (!state.activeSnapshot) return 0

Expand Down Expand Up @@ -178,19 +216,29 @@ function updateSnapshots(newSnapshots: T.Snapshot[]) {
if (snapshot) snapshots.push(snapshot)
}

let activeSnapshot = snapshots.find(s => s.id === state.activeSnapshot?.id) ?? null
const activeSnapshot = snapshots.find(s => s.id === state.activeSnapshot?.id) ?? null
if (!activeSnapshot) resetSelection(state.activeSnapshot)
if (activeSnapshot) activeSnapshot = state.activeSnapshot

state.snapshots = snapshots
state.activeSnapshot = activeSnapshot ?? snapshots[0]
state.activeSnapshot = activeSnapshot ?? snapshots[0] ?? null
}
SetupPage.snapshotsViewer.refresh = updateSnapshots

function activateSnapshot(snapshot?: T.SnapshotState): void {
if (!snapshot || state.activeSnapshot === snapshot) return
resetSelection(state.activeSnapshot)
state.activeSnapshot = snapshot
if (!snapshot) return
if (state.activeSnapshot !== snapshot) {
resetSelection(state.activeSnapshot)
state.activeSnapshot = snapshot
}
if (snapshot.permanent) {
nextTick(() => {
if (titleInputRef.value) {
titleInputRef.value.focus()
const len = titleInputRef.value.value.length
titleInputRef.value.setSelectionRange(len, len)
}
})
}
}

function onHeaderWheel(e: WheelEvent): void {
Expand Down Expand Up @@ -351,6 +399,10 @@ async function openWindow(
}

async function removeSnapshot(snapshot: T.SnapshotState): Promise<void> {
if (snapshot.permanent) {
if (!window.confirm(translate('snapshot.confirm_delete_permanent'))) return
}

const result = await IPC.bg('removeSnapshot', snapshot.id)

if (result === RemovingSnapshotResult.Ok) {
Expand Down Expand Up @@ -468,4 +520,28 @@ function importSnapshot(importEvent: Event) {
}
reader.readAsText(file)
}

async function doMakePermanent(snapshot: T.SnapshotState | null): Promise<void> {
if (!snapshot) return
const result = await Snapshots.makeSnapshotPermanent(snapshot.id)
if (result === RemovingSnapshotResult.Err) Logs.warn('Snapshots: Cannot makeSnapshotPermanent')
}

async function doMakeTemporary(snapshot: T.SnapshotState | null): Promise<void> {
if (!snapshot) return
const result = await Snapshots.makeSnapshotTemporary(snapshot.id)
if (result === RemovingSnapshotResult.Err) Logs.warn('Snapshots: Cannot makeSnapshotTemporary')
}

async function onRenameSnapshot(snapshot: T.SnapshotState, event: Event): Promise<void> {
const title = (event.target as HTMLInputElement).value.trim()
snapshot.title = title || undefined
const result = await Snapshots.renameSnapshot(snapshot.id, title)
if (result === RemovingSnapshotResult.Err) Logs.warn('Snapshots: Cannot renameSnapshot')
}

async function onRenameActiveSnapshot(event: Event): Promise<void> {
if (!state.activeSnapshot) return
await onRenameSnapshot(state.activeSnapshot, event)
}
</script>
86 changes: 83 additions & 3 deletions src/services/snapshots.bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<RemovingSnapshotResult> {
let stored
try {
stored = await browser.storage.local.get<Stored>(['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<RemovingSnapshotResult> {
let stored
try {
stored = await browser.storage.local.get<Stored>(['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<RemovingSnapshotResult> {
let stored
try {
stored = await browser.storage.local.get<Stored>(['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<RemovingSnapshotResult> {
Expand Down
13 changes: 13 additions & 0 deletions src/services/snapshots.fg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -268,6 +269,18 @@ export async function addSnapshot(snapshot: T.NormalizedSnapshot): Promise<void>
return await IPC.bg('addSnapshot', snapshot)
}

export async function makeSnapshotPermanent(id: ID): Promise<RemovingSnapshotResult> {
return await IPC.bg('makeSnapshotPermanent', id)
}

export async function makeSnapshotTemporary(id: ID): Promise<RemovingSnapshotResult> {
return await IPC.bg('makeSnapshotTemporary', id)
}

export async function renameSnapshot(id: ID, title: string): Promise<RemovingSnapshotResult> {
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) {
Expand Down
47 changes: 47 additions & 0 deletions src/styles/page.setup/snapshots.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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
// -
Expand Down
Loading
Loading