Skip to content

Commit 2b16a27

Browse files
committed
Release v0.4.18-beta.4
1 parent 19194ed commit 2b16a27

11 files changed

Lines changed: 199 additions & 38 deletions

File tree

app/backend/db/queries.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -266,36 +266,44 @@ function removeOrphanTags(): void {
266266
/** Removes library records only. Original files on disk are never changed. */
267267
export function cleanupResources(mode: ResourceCleanupMode): ResourceCleanupResult {
268268
const db = getDb()
269+
const missingResourceIds = getAllResources()
270+
.filter(resource => isLocalPath(resource.file_path) && !existsSync(resource.file_path))
271+
.map(resource => resource.id)
272+
const missingIgnoredPaths = getAllIgnoredPaths()
273+
.filter(filePath => isLocalPath(filePath) && !existsSync(filePath))
274+
275+
const removeMissingResources = () => {
276+
if (missingResourceIds.length === 0) return 0
277+
const placeholders = missingResourceIds.map(() => '?').join(', ')
278+
return db.prepare(`DELETE FROM resources WHERE id IN (${placeholders})`).run(...missingResourceIds).changes
279+
}
280+
const removeMissingIgnoredPaths = () => {
281+
if (missingIgnoredPaths.length === 0) return 0
282+
const remove = db.prepare('DELETE FROM ignored_paths WHERE path = ?')
283+
for (const filePath of missingIgnoredPaths) remove.run(filePath)
284+
return missingIgnoredPaths.length
285+
}
269286

270287
if (mode === 'all') {
271288
return db.transaction(() => {
272-
const resources = db.prepare('DELETE FROM resources').run().changes
289+
const resources = removeMissingResources()
290+
const ignored = removeMissingIgnoredPaths()
273291
removeOrphanTags()
274-
return { resources, ignored: 0 }
292+
return { resources, ignored }
275293
})()
276294
}
277295

278296
if (mode === 'missing') {
279-
const ids = getAllResources()
280-
.filter(resource => isLocalPath(resource.file_path) && !existsSync(resource.file_path))
281-
.map(resource => resource.id)
282-
if (ids.length === 0) return { resources: 0, ignored: 0 }
297+
if (missingResourceIds.length === 0) return { resources: 0, ignored: 0 }
283298
return db.transaction(() => {
284-
const placeholders = ids.map(() => '?').join(', ')
285-
db.prepare(`DELETE FROM resources WHERE id IN (${placeholders})`).run(...ids)
299+
const resources = removeMissingResources()
286300
removeOrphanTags()
287-
return { resources: ids.length, ignored: 0 }
301+
return { resources, ignored: 0 }
288302
})()
289303
}
290304

291-
const ignoredPaths = getAllIgnoredPaths()
292-
.filter(filePath => isLocalPath(filePath) && !existsSync(filePath))
293-
if (ignoredPaths.length === 0) return { resources: 0, ignored: 0 }
294-
db.transaction(() => {
295-
const remove = db.prepare('DELETE FROM ignored_paths WHERE path = ?')
296-
for (const filePath of ignoredPaths) remove.run(filePath)
297-
})()
298-
return { resources: 0, ignored: ignoredPaths.length }
305+
if (missingIgnoredPaths.length === 0) return { resources: 0, ignored: 0 }
306+
return { resources: 0, ignored: db.transaction(removeMissingIgnoredPaths)() }
299307
}
300308

301309
// ── 黑名单目录 ───────────────────────────────────────────

app/frontend/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
</main>
8383
</div>
8484
</div>
85+
<ConfirmDialog />
8586
</template>
8687

8788
<script setup lang="ts">
@@ -97,6 +98,7 @@ import MasonryWindow from './components/MasonryWindow.vue'
9798
import TipsMarquee from './components/TipsMarquee.vue'
9899
import { useTips } from './composables/useTips'
99100
import DropImportWindow from './components/DropImportWindow.vue'
101+
import ConfirmDialog from './components/ConfirmDialog.vue'
100102
101103
const windowParam = new URLSearchParams(window.location.search).get('window')
102104
const isMasonryWindow = windowParam === 'masonry'
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<template>
2+
<Teleport to="body">
3+
<Transition name="confirm-fade">
4+
<div v-if="pendingConfirm" class="confirm-overlay" @mousedown.self="settleConfirm(false)">
5+
<section class="confirm-dialog" role="alertdialog" aria-modal="true" :aria-labelledby="titleId" @keydown.esc.prevent="settleConfirm(false)">
6+
<div class="confirm-icon" :class="{ danger: pendingConfirm.danger }" aria-hidden="true">
7+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="m10.3 3.6-8 14A2 2 0 0 0 4 20.5h16a2 2 0 0 0 1.7-3l-8-14a2 2 0 0 0-3.4 0Z"/></svg>
8+
</div>
9+
<div class="confirm-copy">
10+
<h2 :id="titleId">{{ pendingConfirm.title }}</h2>
11+
<p>{{ pendingConfirm.message }}</p>
12+
</div>
13+
<div class="confirm-actions">
14+
<button ref="cancelButton" class="confirm-button secondary" @click="settleConfirm(false)">{{ pendingConfirm.cancelText }}</button>
15+
<button class="confirm-button" :class="{ danger: pendingConfirm.danger }" @click="settleConfirm(true)">{{ pendingConfirm.confirmText }}</button>
16+
</div>
17+
</section>
18+
</div>
19+
</Transition>
20+
</Teleport>
21+
</template>
22+
23+
<script setup lang="ts">
24+
import { nextTick, ref, watch } from 'vue'
25+
import { pendingConfirm, settleConfirm } from '../utils/confirm-dialog'
26+
27+
const cancelButton = ref<HTMLButtonElement | null>(null)
28+
const titleId = 'app-confirm-dialog-title'
29+
30+
watch(pendingConfirm, async (pending) => {
31+
if (!pending) return
32+
await nextTick()
33+
cancelButton.value?.focus()
34+
})
35+
</script>
36+
37+
<style scoped>
38+
.confirm-overlay {
39+
position: fixed;
40+
inset: 0;
41+
z-index: 3000;
42+
display: grid;
43+
place-items: center;
44+
padding: 24px;
45+
background: rgba(5, 5, 16, 0.68);
46+
backdrop-filter: blur(4px);
47+
}
48+
.confirm-dialog {
49+
width: min(100%, 390px);
50+
padding: 20px;
51+
background: var(--surface-2);
52+
border: 1px solid var(--border);
53+
border-radius: 8px;
54+
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.4);
55+
outline: none;
56+
}
57+
.confirm-icon {
58+
display: grid;
59+
width: 32px;
60+
height: 32px;
61+
place-items: center;
62+
border: 1px solid color-mix(in srgb, var(--accent) 48%, transparent);
63+
border-radius: 7px;
64+
color: var(--accent);
65+
background: color-mix(in srgb, var(--accent) 12%, transparent);
66+
}
67+
.confirm-icon.danger { color: var(--danger, #ef5350); border-color: color-mix(in srgb, var(--danger, #ef5350) 48%, transparent); background: color-mix(in srgb, var(--danger, #ef5350) 12%, transparent); }
68+
.confirm-icon svg { width: 17px; height: 17px; }
69+
.confirm-copy { margin-top: 13px; }
70+
.confirm-copy h2 { margin: 0; color: var(--text); font-size: 16px; font-weight: 650; }
71+
.confirm-copy p { margin: 7px 0 0; color: var(--text-2); font-size: 13px; line-height: 1.55; white-space: pre-wrap; }
72+
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
73+
.confirm-button { min-width: 76px; padding: 7px 13px; border: 1px solid var(--accent); border-radius: 6px; background: var(--accent); color: #fff; font: inherit; font-size: 13px; cursor: pointer; }
74+
.confirm-button:hover { filter: brightness(1.08); }
75+
.confirm-button.secondary { border-color: var(--border); background: var(--surface-3); color: var(--text-2); }
76+
.confirm-button.secondary:hover { color: var(--text); }
77+
.confirm-button.danger { border-color: var(--danger, #ef5350); background: var(--danger, #ef5350); }
78+
.confirm-fade-enter-active, .confirm-fade-leave-active { transition: opacity .14s ease; }
79+
.confirm-fade-enter-active .confirm-dialog, .confirm-fade-leave-active .confirm-dialog { transition: transform .14s ease; }
80+
.confirm-fade-enter-from, .confirm-fade-leave-to { opacity: 0; }
81+
.confirm-fade-enter-from .confirm-dialog, .confirm-fade-leave-to .confirm-dialog { transform: translateY(5px) scale(.985); }
82+
</style>

app/frontend/src/i18n/locales/en.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,15 @@ export default {
251251
scanHistory: 'Scan disk',
252252
scanHistoryTitle: 'Scan disk files / read usage history to batch import',
253253
cleanup: 'Clean up',
254-
cleanupTitle: 'Clean up library records',
255-
cleanupAll: 'Clear all',
256-
cleanupAllDesc: 'Clear every resource record from the library',
254+
confirmTitle: 'Confirm action',
255+
cleanupTitle: 'Clean up stale entries',
256+
cleanupAll: 'Clean up all',
257+
cleanupAllDesc: 'Clean up missing resources and ignored paths',
257258
cleanupMissing: 'Clear missing resources',
258259
cleanupMissingDesc: 'Only clear records whose original file is missing',
259260
cleanupIgnoredMissing: 'Clear missing ignored entries',
260261
cleanupIgnoredMissingDesc: 'Remove missing paths from the ignored list',
261-
cleanupConfirmAll: 'Clear every resource record from the library? Original files on your computer will not be deleted.',
262+
cleanupConfirmAll: 'Clean up all missing resources and ignored paths? Original files on your computer will not be deleted.',
262263
cleanupConfirmMissing: 'Clear all missing resource records? Original files on your computer will not be deleted.',
263264
cleanupConfirmIgnoredMissing: 'Clear all missing paths from the ignored list?',
264265
cleanupDone: 'Cleaned {n} items',

app/frontend/src/i18n/locales/zh.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,15 @@ export default {
251251
scanHistory: '扫描硬盘',
252252
scanHistoryTitle: '扫描硬盘文件 / 读取使用历史,批量导入资源',
253253
cleanup: '清理',
254-
cleanupTitle: '清理资源库记录',
255-
cleanupAll: '全部清理',
256-
cleanupAllDesc: '清除资源库中的全部资源记录',
254+
confirmTitle: '确认操作',
255+
cleanupTitle: '清理失效内容',
256+
cleanupAll: '一键清理',
257+
cleanupAllDesc: '清理已失效资源和已失效忽略路径',
257258
cleanupMissing: '清理已失效资源',
258259
cleanupMissingDesc: '只清除找不到原文件的资源记录',
259260
cleanupIgnoredMissing: '清理失效的已忽略内容',
260261
cleanupIgnoredMissingDesc: '从忽略列表移除已不存在的路径',
261-
cleanupConfirmAll: '清除资源库中的全部资源记录?电脑中的原文件不会被删除。',
262+
cleanupConfirmAll: '清理全部失效资源和失效忽略路径?电脑中的原文件不会被删除。',
262263
cleanupConfirmMissing: '清除所有已失效资源记录?电脑中的原文件不会被删除。',
263264
cleanupConfirmIgnoredMissing: '从忽略列表清除所有已失效路径?',
264265
cleanupDone: '已清理 {n} 项',

app/frontend/src/pages/LibraryPage.vue

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,7 @@
206206
</button>
207207
<div class="cleanup-wrap">
208208
<button class="scan-sys-toolbar-btn cleanup-trigger" @click="showCleanupMenu = !showCleanupMenu" :title="t('library.cleanupTitle')">
209-
<span class="btn-icon" v-html="deleteSvg" />
210-
<span class="btn-text">{{ t('library.cleanup') }}</span>
211-
<svg width="9" height="9" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M2 3.5 5 6.5 8 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
209+
<span class="btn-icon" v-html="cleanupSvg" />
212210
</button>
213211
<div v-if="showCleanupMenu" class="cleanup-menu">
214212
<button class="cleanup-menu-item cleanup-menu-item--danger" :disabled="cleanupBusy" @click="runCleanup('all')">
@@ -1822,6 +1820,7 @@ import DropImportModal from '../components/DropImportModal.vue'
18221820
import type { DropItem } from '../components/DropImportModal.vue'
18231821
import ResourceDetailPanel from '../components/ResourceDetailPanel.vue'
18241822
import { match as pinyinMatch } from 'pinyin-pro'
1823+
import { showConfirm } from '../utils/confirm-dialog'
18251824

18261825
const { t, locale } = useI18n()
18271826
const store = useResourceStore()
@@ -4187,6 +4186,7 @@ const typeSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentCol
41874186
const pathSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>`
41884187
const ignoreSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>`
41894188
const deleteSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`
4189+
const cleanupSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m9 11 6-6"/><path d="m5 21 3-3"/><path d="m14 4 3 3"/><path d="m3 7 7 7"/><path d="M5 21h8a3 3 0 0 0 3-3v-5"/></svg>`
41904190
const arrowSvg = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="M13 6l6 6-6 6"/></svg>`
41914191
const aiSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2l2.4 7.2H22l-6 4.8 2.4 7.2L12 16.4l-6.4 4.8 2.4-7.2-6-4.8h7.6z"/></svg>`
41924192
const scanSysSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`
@@ -4209,7 +4209,14 @@ async function runCleanup(mode: CleanupMode) {
42094209
missing: 'library.cleanupConfirmMissing',
42104210
ignoredMissing: 'library.cleanupConfirmIgnoredMissing',
42114211
}
4212-
if (!window.confirm(t(confirmKey[mode]))) return
4212+
showCleanupMenu.value = false
4213+
if (!await showConfirm({
4214+
title: t('library.confirmTitle'),
4215+
message: t(confirmKey[mode]),
4216+
confirmText: t('library.confirmBtn'),
4217+
cancelText: t('library.cancelBtn'),
4218+
danger: true,
4219+
})) return
42134220

42144221
cleanupBusy.value = true
42154222
cleanupNotice.value = ''
@@ -4780,8 +4787,14 @@ async function openLocalNote(resource: Resource, touchUsage = true) {
47804787
noteSurfaceRef.value?.focus()
47814788
}
47824789

4783-
function closeNoteEditor() {
4784-
if (noteEditor.dirty && !confirm(t('library.documents.discardConfirm'))) return
4790+
async function closeNoteEditor() {
4791+
if (noteEditor.dirty && !await showConfirm({
4792+
title: t('library.confirmTitle'),
4793+
message: t('library.documents.discardConfirm'),
4794+
confirmText: t('library.confirmBtn'),
4795+
cancelText: t('library.cancelBtn'),
4796+
danger: true,
4797+
})) return
47854798
noteEditor.show = false
47864799
noteEditor.resource = null
47874800
noteEditor.blocks = []
@@ -5425,7 +5438,7 @@ async function deleteIgnored(filePath: string) {
54255438
.scan-sys-toolbar-btn .btn-icon { width: 16px; height: 16px; }
54265439

54275440
.cleanup-wrap { position: relative; }
5428-
.cleanup-trigger { gap: 4px; }
5441+
.cleanup-trigger { width: 30px; justify-content: center; padding: 5px; }
54295442
.cleanup-menu {
54305443
position: absolute;
54315444
z-index: 120;

app/frontend/src/pages/SettingsPage.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@
634634
<script setup lang="ts">
635635
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
636636
import { useI18n } from 'vue-i18n'
637+
import { showConfirm } from '../utils/confirm-dialog'
637638
import { useSettingsStore, DARK_THEME, COLOR_PALETTES } from '../stores/settings'
638639
import type { PaletteId, BrightnessMode } from '../stores/settings'
639640
@@ -910,7 +911,13 @@ async function onCreateProfile() {
910911
async function onDeleteProfile() {
911912
if (activeProfileId.value === 'default' || profiles.value.length <= 1) return
912913
const current = profiles.value.find(p => p.id === activeProfileId.value)
913-
if (!confirm(t('settings.data.deleteConfirm', { name: current?.name }))) return
914+
if (!await showConfirm({
915+
title: t('library.confirmTitle'),
916+
message: t('settings.data.deleteConfirm', { name: current?.name }),
917+
confirmText: t('library.confirmBtn'),
918+
cancelText: t('library.cancelBtn'),
919+
danger: true,
920+
})) return
914921
await window.api.profiles.delete(activeProfileId.value)
915922
// 切换到第一个剩余配置
916923
const remaining = profiles.value.filter(p => p.id !== activeProfileId.value)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ref } from 'vue'
2+
3+
export interface ConfirmDialogOptions {
4+
title: string
5+
message: string
6+
confirmText: string
7+
cancelText: string
8+
danger?: boolean
9+
}
10+
11+
interface PendingConfirm extends ConfirmDialogOptions {
12+
resolve: (accepted: boolean) => void
13+
}
14+
15+
export const pendingConfirm = ref<PendingConfirm | null>(null)
16+
17+
export function showConfirm(options: ConfirmDialogOptions): Promise<boolean> {
18+
if (pendingConfirm.value) pendingConfirm.value.resolve(false)
19+
return new Promise(resolve => {
20+
pendingConfirm.value = { ...options, resolve }
21+
})
22+
}
23+
24+
export function settleConfirm(accepted: boolean): void {
25+
const pending = pendingConfirm.value
26+
if (!pending) return
27+
pendingConfirm.value = null
28+
pending.resolve(accepted)
29+
}

app/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ai-cubby",
3-
"version": "0.4.18-beta.3",
3+
"version": "0.4.18-beta.4",
44
"description": "AI小抽屉 - 本地资源自动入库与AI标签",
55
"main": "out/main/main.js",
66
"scripts": {

0 commit comments

Comments
 (0)