From 33c421b7464a424367ffcbfb21d5ddda00122aa6 Mon Sep 17 00:00:00 2001 From: Iisyourdad Date: Thu, 11 Jun 2026 11:31:58 -0500 Subject: [PATCH] Added select button toggle for trash library grid --- app/main.js | 6 ++- app/renderer/app.js | 95 ++++++++++++++++++++++++++++++++++++++------- core/store.js | 6 +++ docs/CHANGELOG.md | 3 ++ 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/app/main.js b/app/main.js index 7ece400..2d211aa 100644 --- a/app/main.js +++ b/app/main.js @@ -202,7 +202,11 @@ function setupIpc() { reindex(id); return id; }); - h('library:trash:purge', () => store.purgeTrash()); + h('library:trash:purge', ({ names } = {}) => { + if (names && names.length) store.purgeTrashItems(names); + else store.purgeTrash(); + return true; + }); h('folders:create', ({ name, parentId }) => store.createFolder(name, parentId || null)); h('folders:rename', ({ folderId, name }) => store.renameFolder(folderId, name)); h('folders:delete', ({ folderId }) => store.deleteFolder(folderId)); diff --git a/app/renderer/app.js b/app/renderer/app.js index 6560d8f..acf15aa 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -23,6 +23,7 @@ class StepForgeApp { info: null, selectMode: false, selectedGuides: new Set(), + selectedTrash: new Set(), }; this.editorMeta = null; this.libraryRenderToken = 0; @@ -351,12 +352,13 @@ class StepForgeApp { clearNode(this.libraryHost); const q = this.state.query.trim(); const folderLabel = this.filterLabel(); - // Selecting guides only makes sense for the plain guide grid — drop out - // of select mode for search results and the trash. - const canSelect = !q && this.state.folderFilter !== 'trash'; + // Selecting only makes sense for the guide grid and the trash — drop out + // of select mode for search results. + const canSelect = !q; if (!canSelect && this.state.selectMode) { this.state.selectMode = false; this.state.selectedGuides = new Set(); + this.state.selectedTrash = new Set(); } const body = el('div.library', {}, el('aside.lib-side', {}, @@ -406,10 +408,18 @@ class StepForgeApp { this.renderTopbar(); } + setFolderFilter(folderFilter) { + this.state.folderFilter = folderFilter; + this.state.selectMode = false; + this.state.selectedGuides = new Set(); + this.state.selectedTrash = new Set(); + this.renderLibrary(); + } + libraryNavItem(id, label, count) { const props = { className: `nav-item${this.state.folderFilter === id ? ' active' : ''}`, - onClick: () => { this.state.folderFilter = id; this.renderLibrary(); }, + onClick: () => this.setFolderFilter(id), }; if (!['all', 'favorites', 'trash'].includes(id)) { props.onContextMenu = (e) => this.folderContextMenu(e, id); @@ -430,7 +440,7 @@ class StepForgeApp { out.push(el('div.nav-item', { className: `nav-item${this.state.folderFilter === folder.id ? ' active' : ''}`, style: { paddingLeft: `${8 + depth * 12}px` }, - onClick: () => { this.state.folderFilter = folder.id; this.renderLibrary(); }, + onClick: () => this.setFolderFilter(folder.id), onContextMenu: (e) => this.folderContextMenu(e, folder.id), }, el('span', {}, folder.name), @@ -514,17 +524,26 @@ class StepForgeApp { this.domLibraryResults.append(el('div.empty-state', {}, el('div.big', {}, 'Trash'), 'Nothing deleted yet.')); return; } - const items = this.state.trash.map((name) => el('div.guide-card', { - onContextMenu: (e) => { - e.preventDefault(); - contextMenu(e.clientX, e.clientY, [ - { label: 'Restore', action: () => this.restoreTrashItem(name) }, - { label: 'Empty trash', danger: true, action: () => this.purgeTrashItem() }, - ]); + const selectMode = this.state.selectMode; + const items = this.state.trash.map((name) => { + const selected = this.state.selectedTrash.has(name); + return el('div.guide-card', { + className: `guide-card${selected ? ' selected' : ''}`, + onClick: () => { + if (selectMode) this.toggleTrashSelection(name); + }, + onContextMenu: (e) => { + e.preventDefault(); + if (selectMode) return; + contextMenu(e.clientX, e.clientY, [ + { label: 'Restore', action: () => this.restoreTrashItem(name) }, + { label: 'Empty trash', danger: true, action: () => this.purgeTrashItem() }, + ]); + }, }, - }, - el('h4', {}, name), - el('div.meta', {}, 'Deleted guide archive'))); + el('h4', {}, name), + el('div.meta', {}, 'Deleted guide archive')); + }); this.domLibraryResults.append(el('div.guide-grid', {}, ...items)); } @@ -604,6 +623,7 @@ class StepForgeApp { toggleSelectMode() { this.state.selectMode = !this.state.selectMode; this.state.selectedGuides = new Set(); + this.state.selectedTrash = new Set(); this.renderLibrary(); } @@ -627,10 +647,55 @@ class StepForgeApp { this.renderBulkBar(); } + toggleTrashSelection(name) { + if (this.state.selectedTrash.has(name)) this.state.selectedTrash.delete(name); + else this.state.selectedTrash.add(name); + this.renderTrashView(); + this.renderBulkBar(); + } + + selectAllTrash() { + this.state.selectedTrash = new Set(this.state.trash); + this.renderTrashView(); + this.renderBulkBar(); + } + + clearTrashSelection() { + this.state.selectedTrash = new Set(); + this.renderTrashView(); + this.renderBulkBar(); + } + + async bulkPurgeTrash() { + const names = [...this.state.selectedTrash]; + if (!names.length) return; + const ok = await confirmDialog(`Permanently delete ${names.length} item${names.length === 1 ? '' : 's'}? This cannot be undone.`, { danger: true, okLabel: 'Delete forever' }); + if (!ok) return; + await api.library.trashPurge({ names }); + this.state.selectedTrash = new Set(); + await this.refreshLibrary(); + } + renderBulkBar() { if (!this.domBulkBar) return; clearNode(this.domBulkBar); if (!this.state.selectMode) return; + if (this.state.folderFilter === 'trash') { + const n = this.state.selectedTrash.size; + const allSelected = this.state.trash.length > 0 && n === this.state.trash.length; + this.domBulkBar.append( + el('div.bulk-bar', {}, + el('span', {}, n ? `${n} selected` : 'Select items to delete forever'), + el('span.spacer', {}), + el('button', { + type: 'button', + onClick: () => (allSelected ? this.clearTrashSelection() : this.selectAllTrash()), + }, allSelected ? 'Clear selection' : 'Select all'), + el('button.danger', { type: 'button', disabled: !n, onClick: () => this.bulkPurgeTrash() }, 'Delete forever'), + ), + ); + return; + } const guides = this.state.library.guides.filter((guide) => this.scopeGuide(guide)); const n = this.state.selectedGuides.size; const allSelected = guides.length > 0 && n === guides.length; diff --git a/core/store.js b/core/store.js index 8117b15..8c25661 100644 --- a/core/store.js +++ b/core/store.js @@ -134,6 +134,12 @@ class GuideStore { } } + purgeTrashItems(names) { + for (const name of names) { + fs.rmSync(path.join(this.trashDir, path.basename(name)), { recursive: true, force: true }); + } + } + duplicateGuide(guideId, { title } = {}) { const src = this.getGuide(guideId); const steps = this.listSteps(guideId); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cd1bae2..7dd77a5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -58,6 +58,9 @@ Initial release. recording" option to resume capturing more steps. - Editor step list: a "Select" toggle enables multi-select (checkboxes) with a "Select all" / "Delete" bar for removing several steps at once. +- The library's "Select" toggle and bulk action bar now also work in the + Trash, with a "Delete forever" action that permanently removes the + selected items. ### Fixed