mirror of
https://github.com/gapmiss/substack-clipper.git
synced 2026-07-22 07:48:24 +00:00
feat: convert save history from modal to sidebar view with select all
This commit is contained in:
parent
8517794292
commit
b5aaa9a1a9
3 changed files with 124 additions and 23 deletions
|
|
@ -1,25 +1,37 @@
|
|||
import { Modal, normalizePath, Notice, setIcon, TFile } from 'obsidian';
|
||||
import { ItemView, normalizePath, Notice, setIcon, TFile, type WorkspaceLeaf } from 'obsidian';
|
||||
import type SubstackClipperPlugin from './main';
|
||||
import type { HistoryEntry } from './types';
|
||||
import { fetchComments, renderComments } from './comments';
|
||||
|
||||
export class HistoryModal extends Modal {
|
||||
export const HISTORY_VIEW_TYPE = 'substack-clipper-history';
|
||||
|
||||
export class HistoryView extends ItemView {
|
||||
private plugin: SubstackClipperPlugin;
|
||||
private filterText = '';
|
||||
private listEl: HTMLElement;
|
||||
private statusEl: HTMLElement;
|
||||
private selectedEntries: Set<number> = new Set();
|
||||
|
||||
constructor(plugin: SubstackClipperPlugin) {
|
||||
super(plugin.app);
|
||||
constructor(leaf: WorkspaceLeaf, plugin: SubstackClipperPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.setTitle('Substack save history');
|
||||
this.modalEl.addClass('substack-clipper-history-modal');
|
||||
getViewType(): string {
|
||||
return HISTORY_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Save history';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'history';
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('substack-clipper-history-view');
|
||||
|
||||
const searchInput = contentEl.createEl('input', {
|
||||
type: 'text',
|
||||
|
|
@ -51,6 +63,14 @@ export class HistoryModal extends Modal {
|
|||
this.renderList();
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.renderList();
|
||||
}
|
||||
|
||||
private getFilteredEntries(): HistoryEntry[] {
|
||||
const reversed = [...this.plugin.settings.history].reverse();
|
||||
if (!this.filterText) return reversed;
|
||||
|
|
@ -68,29 +88,49 @@ export class HistoryModal extends Modal {
|
|||
|
||||
if (entries.length === 0) {
|
||||
this.listEl.createDiv({
|
||||
text: 'No matching entries.',
|
||||
text: this.plugin.settings.history.length === 0
|
||||
? 'No saved posts yet.'
|
||||
: 'No matching entries.',
|
||||
cls: 'substack-clipper-history-empty',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const header = this.listEl.createDiv({ cls: 'substack-clipper-history-header' });
|
||||
const selectAllCb = header.createEl('input', { type: 'checkbox' });
|
||||
selectAllCb.setAttribute('aria-label', 'Select all entries');
|
||||
header.createEl('span', {
|
||||
text: `${String(entries.length)} ${entries.length === 1 ? 'entry' : 'entries'}`,
|
||||
cls: 'substack-clipper-history-count',
|
||||
});
|
||||
|
||||
const rowCheckboxes: { checkbox: HTMLInputElement; idx: number }[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const row = this.listEl.createDiv({ cls: 'substack-clipper-history-row' });
|
||||
|
||||
const checkbox = row.createEl('input', { type: 'checkbox' });
|
||||
checkbox.setAttribute('aria-label', `Select ${entry.title}`);
|
||||
const idx = this.plugin.settings.history.indexOf(entry);
|
||||
rowCheckboxes.push({ checkbox, idx });
|
||||
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) {
|
||||
this.selectedEntries.add(idx);
|
||||
} else {
|
||||
this.selectedEntries.delete(idx);
|
||||
}
|
||||
const total = rowCheckboxes.length;
|
||||
const checked = rowCheckboxes.filter(r => r.checkbox.checked).length;
|
||||
selectAllCb.checked = checked === total;
|
||||
selectAllCb.indeterminate = checked > 0 && checked < total;
|
||||
});
|
||||
|
||||
const info = row.createDiv({ cls: 'substack-clipper-history-info' });
|
||||
info.createEl('span', { text: entry.title, cls: 'substack-clipper-history-title' });
|
||||
const userLine = info.createDiv({ cls: 'substack-clipper-history-userline' });
|
||||
|
||||
const meta = info.createDiv({ cls: 'substack-clipper-history-meta' });
|
||||
const userLine = meta.createDiv({ cls: 'substack-clipper-history-userline' });
|
||||
userLine.createEl('span', { text: entry.username, cls: 'substack-clipper-history-username' });
|
||||
if (entry.commentCount > 0) {
|
||||
userLine.createEl('span', {
|
||||
|
|
@ -99,12 +139,12 @@ export class HistoryModal extends Modal {
|
|||
});
|
||||
}
|
||||
|
||||
row.createEl('span', {
|
||||
meta.createEl('span', {
|
||||
text: new Date(entry.dateSaved).toLocaleDateString(),
|
||||
cls: 'substack-clipper-history-date',
|
||||
});
|
||||
|
||||
const actions = row.createDiv({ cls: 'substack-clipper-history-actions' });
|
||||
const actions = meta.createDiv({ cls: 'substack-clipper-history-actions' });
|
||||
|
||||
const noteBtn = actions.createEl('button', { cls: 'clickable-icon' });
|
||||
noteBtn.setAttribute('aria-label', 'Open saved note');
|
||||
|
|
@ -117,7 +157,6 @@ export class HistoryModal extends Modal {
|
|||
const file = this.app.vault.getAbstractFileByPath(notePath);
|
||||
if (file instanceof TFile) {
|
||||
void this.app.workspace.getLeaf(false).openFile(file);
|
||||
this.close();
|
||||
} else {
|
||||
new Notice('Note not found in vault.');
|
||||
}
|
||||
|
|
@ -141,6 +180,18 @@ export class HistoryModal extends Modal {
|
|||
this.renderList();
|
||||
});
|
||||
}
|
||||
|
||||
selectAllCb.addEventListener('change', () => {
|
||||
const checked = selectAllCb.checked;
|
||||
for (const { checkbox, idx } of rowCheckboxes) {
|
||||
checkbox.checked = checked;
|
||||
if (checked) {
|
||||
this.selectedEntries.add(idx);
|
||||
} else {
|
||||
this.selectedEntries.delete(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async redownloadComments(btn: HTMLButtonElement): Promise<void> {
|
||||
26
src/main.ts
26
src/main.ts
|
|
@ -3,7 +3,7 @@ import { type SubstackClipperSettings, DEFAULT_SETTINGS } from './types';
|
|||
import type { ParsedArticle, DownloadResult, HistoryEntry } from './types';
|
||||
import { SettingsTab } from './settings';
|
||||
import { ClipModal } from './modal';
|
||||
import { HistoryModal } from './history-modal';
|
||||
import { HistoryView, HISTORY_VIEW_TYPE } from './history-view';
|
||||
import { fetchHtml, extractPreloads, parsePost, parseUrl, extractImages, extractVideos, extractAudios, extractAttachments } from './parser';
|
||||
import { htmlToMarkdown } from './converter';
|
||||
import { postprocessMarkdown } from './postprocess';
|
||||
|
|
@ -17,6 +17,8 @@ export default class SubstackClipperPlugin extends Plugin {
|
|||
await this.loadSettings();
|
||||
this.addSettingTab(new SettingsTab(this.app, this));
|
||||
|
||||
this.registerView(HISTORY_VIEW_TYPE, (leaf) => new HistoryView(leaf, this));
|
||||
|
||||
this.addCommand({
|
||||
id: 'clip-post',
|
||||
name: 'Save substack post',
|
||||
|
|
@ -33,13 +35,25 @@ export default class SubstackClipperPlugin extends Plugin {
|
|||
|
||||
this.addCommand({
|
||||
id: 'view-history',
|
||||
name: 'View history',
|
||||
name: 'View save history',
|
||||
callback: () => {
|
||||
new HistoryModal(this).open();
|
||||
void this.activateHistoryView();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async activateHistoryView(): Promise<void> {
|
||||
const { workspace } = this.app;
|
||||
let leaf = workspace.getLeavesOfType(HISTORY_VIEW_TYPE)[0];
|
||||
if (!leaf) {
|
||||
const rightLeaf = workspace.getRightLeaf(false);
|
||||
if (!rightLeaf) return;
|
||||
await rightLeaf.setViewState({ type: HISTORY_VIEW_TYPE, active: true });
|
||||
leaf = rightLeaf;
|
||||
}
|
||||
await workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<SubstackClipperSettings>);
|
||||
}
|
||||
|
|
@ -228,6 +242,12 @@ export default class SubstackClipperPlugin extends Plugin {
|
|||
}
|
||||
await this.saveSettings();
|
||||
|
||||
for (const leaf of this.app.workspace.getLeavesOfType(HISTORY_VIEW_TYPE)) {
|
||||
if (leaf.view instanceof HistoryView) {
|
||||
leaf.view.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
notice.hide();
|
||||
new Notice(`Save failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||
|
|
|
|||
46
styles.css
46
styles.css
|
|
@ -11,10 +11,17 @@
|
|||
width: 60px;
|
||||
}
|
||||
|
||||
.substack-clipper-history-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: var(--size-4-3);
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
.substack-clipper-history-search {
|
||||
width: 100%;
|
||||
padding: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-2);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background: var(--background-primary);
|
||||
|
|
@ -23,16 +30,33 @@
|
|||
}
|
||||
|
||||
.substack-clipper-history-list {
|
||||
max-height: 400px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.substack-clipper-history-row {
|
||||
.substack-clipper-history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-2);
|
||||
padding: var(--size-4-1) var(--size-4-3);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-secondary);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.substack-clipper-history-count {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.substack-clipper-history-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--size-4-2);
|
||||
padding: var(--size-4-2) var(--size-4-3);
|
||||
min-height: 44px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
|
|
@ -47,19 +71,24 @@
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
|
||||
.substack-clipper-history-title {
|
||||
font-weight: var(--font-semibold);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.substack-clipper-history-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-4-1) var(--size-4-2);
|
||||
}
|
||||
|
||||
.substack-clipper-history-userline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-2);
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
|
||||
.substack-clipper-history-username {
|
||||
|
|
@ -88,8 +117,9 @@
|
|||
|
||||
.substack-clipper-history-actions {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.substack-clipper-history-empty {
|
||||
|
|
|
|||
Loading…
Reference in a new issue