From 0129a2aca995c212a60b6d3e746ddaf4d142fb06 Mon Sep 17 00:00:00 2001 From: "@gapmiss" Date: Sat, 11 Jul 2026 16:38:33 -0500 Subject: [PATCH] feat: improve history view with search icon/clear button, Notice toasts, and disabled state during re-download --- src/history-view.ts | 75 ++++++++++++++++++++++++---------- styles.css | 98 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 141 insertions(+), 32 deletions(-) diff --git a/src/history-view.ts b/src/history-view.ts index 2b246b2..5b93156 100644 --- a/src/history-view.ts +++ b/src/history-view.ts @@ -9,7 +9,7 @@ export class HistoryView extends ItemView { private plugin: SubstackClipperPlugin; private filterText = ''; private listEl: HTMLElement; - private statusEl: HTMLElement; + private redownloadBtn: HTMLButtonElement; private selectedEntries: Set = new Set(); constructor(leaf: WorkspaceLeaf, plugin: SubstackClipperPlugin) { @@ -33,31 +33,49 @@ export class HistoryView extends ItemView { const { contentEl } = this; contentEl.addClass('substack-clipper-history-view'); - const searchInput = contentEl.createEl('input', { + const searchContainer = contentEl.createDiv({ cls: 'substack-clipper-history-search-container' }); + const searchIconEl = searchContainer.createSpan({ cls: 'substack-clipper-history-search-icon' }); + setIcon(searchIconEl, 'search'); + const searchInput = searchContainer.createEl('input', { type: 'text', - placeholder: 'Filter by title or author...', cls: 'substack-clipper-history-search', + attr: { + placeholder: 'Filter by title or author...', + 'aria-label': 'Filter history entries', + }, + }); + const clearBtn = searchContainer.createEl('button', { + cls: 'substack-clipper-history-search-clear', + attr: { + 'aria-label': 'Clear filter', + 'data-tooltip-position': 'top', + }, + }); + setIcon(clearBtn, 'x'); + clearBtn.addEventListener('click', () => { + searchInput.value = ''; + this.filterText = ''; + clearBtn.removeClass('is-visible'); + this.renderList(); + searchInput.focus(); }); - searchInput.setAttribute('aria-label', 'Filter history entries'); searchInput.addEventListener('input', () => { this.filterText = searchInput.value.toLowerCase(); + clearBtn.toggleClass('is-visible', searchInput.value.length > 0); this.renderList(); }); this.listEl = contentEl.createDiv({ cls: 'substack-clipper-history-list' }); - this.statusEl = contentEl.createDiv({ cls: 'substack-clipper-history-status' }); - this.statusEl.setAttribute('role', 'status'); - this.statusEl.setAttribute('aria-live', 'polite'); - const footer = contentEl.createDiv({ cls: 'substack-clipper-history-footer' }); - const btn = footer.createEl('button', { + this.redownloadBtn = footer.createEl('button', { text: 'Re-download comments', cls: 'mod-cta', + attr: { 'aria-label': 'Re-download comments for selected entries' }, }); - btn.setAttribute('aria-label', 'Re-download comments for selected entries'); - btn.addEventListener('click', () => { - void this.redownloadComments(btn); + this.redownloadBtn.disabled = true; + this.redownloadBtn.addEventListener('click', () => { + void this.redownloadComments(); }); this.renderList(); @@ -80,9 +98,14 @@ export class HistoryView extends ItemView { ); } + private syncRedownloadBtn(): void { + this.redownloadBtn.disabled = this.selectedEntries.size === 0; + } + private renderList(): void { this.listEl.empty(); this.selectedEntries.clear(); + this.syncRedownloadBtn(); const entries = this.getFilteredEntries(); @@ -124,6 +147,7 @@ export class HistoryView extends ItemView { const checked = rowCheckboxes.filter(r => r.checkbox.checked).length; selectAllCb.checked = checked === total; selectAllCb.indeterminate = checked > 0 && checked < total; + this.syncRedownloadBtn(); }); const info = row.createDiv({ cls: 'substack-clipper-history-info' }); @@ -191,17 +215,27 @@ export class HistoryView extends ItemView { this.selectedEntries.delete(idx); } } + this.syncRedownloadBtn(); }); } - private async redownloadComments(btn: HTMLButtonElement): Promise { + private setViewDisabled(disabled: boolean): void { + this.contentEl.toggleClass('is-disabled', disabled); + const inputs = Array.from(this.contentEl.querySelectorAll('input, button')); + for (const el of inputs) { + el.disabled = disabled; + } + } + + private async redownloadComments(): Promise { if (this.selectedEntries.size === 0) { new Notice('No entries selected.'); return; } - btn.disabled = true; - this.statusEl.setText('Downloading...'); + this.setViewDisabled(true); + const notice = new Notice('Re-downloading comments...', 0); + notice.containerEl.addClass('is-loading'); const history = this.plugin.settings.history; const indices = [...this.selectedEntries]; @@ -233,18 +267,15 @@ export class HistoryView extends ItemView { } await this.plugin.saveSettings(); + notice.hide(); + this.setViewDisabled(false); this.renderList(); - btn.disabled = false; if (failed === 0) { - const msg = `Re-downloaded comments for ${String(succeeded)} posts`; - this.statusEl.setText(msg); - new Notice(msg); + new Notice(`Re-downloaded comments for ${String(succeeded)} posts`); } else { const total = succeeded + failed; - const msg = `Re-downloaded ${String(succeeded)} of ${String(total)} (${String(failed)} failed)`; - this.statusEl.setText(msg); - new Notice(msg); + new Notice(`Re-downloaded ${String(succeeded)} of ${String(total)} (${String(failed)} failed)`); } } } diff --git a/styles.css b/styles.css index 6cfa883..ac31b09 100644 --- a/styles.css +++ b/styles.css @@ -19,16 +19,96 @@ gap: var(--size-4-2); } +.substack-clipper-history-search-container { + display: flex; + align-items: center; + gap: var(--size-4-1); + min-height: 32px; + padding: 0 var(--size-4-2); + background: var(--background-secondary); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--radius-m); +} + +.substack-clipper-history-search-container:focus-within { + box-shadow: inset 0 0 0 1px var(--background-modifier-border-focus); +} + +.substack-clipper-history-search-icon { + display: flex; + align-items: center; + color: var(--text-faint); + flex-shrink: 0; +} + +.substack-clipper-history-search-icon svg { + width: 16px; + height: 16px; +} + .substack-clipper-history-search { - width: 100%; - padding: var(--size-4-2); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - background: var(--background-primary); + flex: 1; + min-width: 0; + padding: var(--size-4-1) 0; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + background: transparent; color: var(--text-normal); font-size: var(--font-ui-medium); } +.substack-clipper-history-search:hover, +.substack-clipper-history-search:focus, +.substack-clipper-history-search:focus-visible { + border: none; + outline: none; + box-shadow: none; + background: transparent; +} + +.substack-clipper-history-view .substack-clipper-history-search-container input[type="text"] { + border: none; + outline: none; + box-shadow: none; + background: transparent; +} + +.substack-clipper-history-search::placeholder { + color: var(--text-faint); +} + +.substack-clipper-history-search-clear { + display: none; + align-items: center; + justify-content: center; + border: none; + border-radius: 50%; + background: transparent; + color: var(--text-faint); + cursor: pointer; + flex-shrink: 0; + height: 20px; + width: 20px; + margin: auto; + padding: 0; +} + +.substack-clipper-history-search-clear:hover { + color: var(--text-muted); + background: var(--background-modifier-hover); +} + +.substack-clipper-history-search-clear.is-visible { + display: flex; +} + +.substack-clipper-history-search-clear svg { + width: 14px; + height: 14px; +} + .substack-clipper-history-list { flex: 1; overflow-y: auto; @@ -128,11 +208,9 @@ text-align: center; } -.substack-clipper-history-status { - padding: var(--size-4-2) 0; - color: var(--text-muted); - font-size: var(--font-ui-small); - min-height: var(--size-4-4); +.substack-clipper-history-view.is-disabled { + pointer-events: none; + opacity: 0.5; } .substack-clipper-history-footer {