mirror of
https://github.com/gapmiss/substack-clipper.git
synced 2026-07-22 07:48:24 +00:00
Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2829630e7c | ||
|
|
7a885e6aa7 | ||
|
|
5070666786 | ||
|
|
ce476bb678 | ||
|
|
abd4235867 | ||
|
|
44d48b31ab | ||
|
|
afd54934cd | ||
|
|
882487103f | ||
|
|
cd4cfb0802 | ||
|
|
cc2300c5d6 | ||
|
|
defd646968 | ||
|
|
c9bd606f4c | ||
|
|
4561bfe699 | ||
|
|
0129a2aca9 | ||
|
|
1c6cfefbbf |
11 changed files with 5483 additions and 5285 deletions
16
README.md
16
README.md
|
|
@ -61,6 +61,22 @@ Substacks/
|
||||||
|
|
||||||
The main note includes frontmatter with title, subtitle, type, audience, date, comment count, and links to all media. Comments are embedded via `![[slug-comments]]`.
|
The main note includes frontmatter with title, subtitle, type, audience, date, comment count, and links to all media. Comments are embedded via `![[slug-comments]]`.
|
||||||
|
|
||||||
|
### URI protocol
|
||||||
|
|
||||||
|
You can save a post directly via an `obsidian://` URI without opening the command palette:
|
||||||
|
|
||||||
|
```
|
||||||
|
obsidian://substack-clipper?vault=MyVault&url=https://alice.substack.com/p/my-article
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Required | Description |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| `vault` | no | Target vault name (Obsidian prompts if omitted and multiple vaults exist) |
|
||||||
|
| `url` | yes | Full Substack post URL (must contain `/p/`) |
|
||||||
|
| `open` | no | `true` or `false` — override the "open after saving" setting |
|
||||||
|
|
||||||
|
Spaces in vault names should be percent-encoded (`%20`). This works from browsers, scripts, iOS Shortcuts, bookmarklets, and anywhere else that can open a URL.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
| Setting | Default | Description |
|
| Setting | Default | Description |
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "substack-clipper",
|
"id": "substack-clipper",
|
||||||
"name": "Substack Clipper",
|
"name": "Substack Clipper",
|
||||||
"version": "0.1.5",
|
"version": "0.1.12",
|
||||||
"minAppVersion": "1.13.0",
|
"minAppVersion": "1.13.0",
|
||||||
"description": "Archive Substack posts as Markdown with images, media, and comments.",
|
"description": "Archive Substack posts as Markdown with images, media, and comments.",
|
||||||
"author": "@gapmiss",
|
"author": "@gapmiss",
|
||||||
|
|
|
||||||
10471
package-lock.json
generated
10471
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "substack-clipper",
|
"name": "substack-clipper",
|
||||||
"version": "0.1.5",
|
"version": "0.1.12",
|
||||||
"description": "Clip substack articles",
|
"description": "Clip substack articles",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.1",
|
||||||
"eslint": "^9.30.1",
|
"eslint": "^9.30.1",
|
||||||
"eslint-plugin-no-unsanitized": "^4.1.2",
|
"eslint-plugin-no-unsanitized": "^4.1.2",
|
||||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
"eslint-plugin-obsidianmd": "^0.4.1",
|
||||||
"globals": "^14.0.0",
|
"globals": "^14.0.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,21 @@ function renderComment(
|
||||||
return md;
|
return md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countNestedComments(comments: Comment[]): number {
|
||||||
|
let total = 0;
|
||||||
|
for (const c of comments) {
|
||||||
|
total += 1;
|
||||||
|
if (c.children) {
|
||||||
|
total += countNestedComments(c.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countAllComments(data: CommentsResponse): number {
|
||||||
|
return countNestedComments(data.comments);
|
||||||
|
}
|
||||||
|
|
||||||
export function renderComments(
|
export function renderComments(
|
||||||
data: CommentsResponse,
|
data: CommentsResponse,
|
||||||
slug: string,
|
slug: string,
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,16 @@ function isSubstackImageUrl(url: string): boolean {
|
||||||
const VIDEO_PLACEHOLDER_RE = /VIDEOEMBED([a-f0-9-]{36})ENDVIDEO/g;
|
const VIDEO_PLACEHOLDER_RE = /VIDEOEMBED([a-f0-9-]{36})ENDVIDEO/g;
|
||||||
|
|
||||||
function replaceVideoDivs(html: string, downloadedUrls: Set<string>): string {
|
function replaceVideoDivs(html: string, downloadedUrls: Set<string>): string {
|
||||||
const parser = new DOMParser();
|
return html.replace(
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
/<div[^>]+id="media-([^"]{36})"[^>]*>[\s\S]*?<\/div>/g,
|
||||||
|
(_match, videoId: string) => {
|
||||||
for (const div of Array.from(doc.querySelectorAll('div[id^="media-"]'))) {
|
const videoUrl = `https://substack.com/api/v1/video/upload/${videoId}/src`;
|
||||||
const id = div.getAttribute('id') ?? '';
|
if (downloadedUrls.has(videoUrl)) {
|
||||||
if (id.length !== 42) continue;
|
return `<p>VIDEOEMBED${videoId}ENDVIDEO</p>`;
|
||||||
const videoId = id.replace('media-', '');
|
}
|
||||||
const videoUrl = `https://substack.com/api/v1/video/upload/${videoId}/src`;
|
return `<p><a href="${videoUrl}">Video</a></p>`;
|
||||||
const p = doc.createElement('p');
|
|
||||||
if (downloadedUrls.has(videoUrl)) {
|
|
||||||
p.textContent = `VIDEOEMBED${videoId}ENDVIDEO`;
|
|
||||||
} else {
|
|
||||||
const a = doc.createElement('a');
|
|
||||||
a.href = videoUrl;
|
|
||||||
a.textContent = 'Video';
|
|
||||||
p.appendChild(a);
|
|
||||||
}
|
}
|
||||||
div.replaceWith(p);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return doc.body.innerHTML;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreVideoWikilinks(md: string): string {
|
function restoreVideoWikilinks(md: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ItemView, normalizePath, Notice, setIcon, TFile, type WorkspaceLeaf } from 'obsidian';
|
import { ItemView, normalizePath, Notice, setIcon, TFile, type WorkspaceLeaf } from 'obsidian';
|
||||||
import type SubstackClipperPlugin from './main';
|
import type SubstackClipperPlugin from './main';
|
||||||
import type { HistoryEntry } from './types';
|
import type { HistoryEntry } from './types';
|
||||||
import { fetchComments, renderComments } from './comments';
|
import { countAllComments, fetchComments, renderComments } from './comments';
|
||||||
|
|
||||||
export const HISTORY_VIEW_TYPE = 'substack-clipper-history';
|
export const HISTORY_VIEW_TYPE = 'substack-clipper-history';
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ export class HistoryView extends ItemView {
|
||||||
private plugin: SubstackClipperPlugin;
|
private plugin: SubstackClipperPlugin;
|
||||||
private filterText = '';
|
private filterText = '';
|
||||||
private listEl: HTMLElement;
|
private listEl: HTMLElement;
|
||||||
private statusEl: HTMLElement;
|
private redownloadBtn: HTMLButtonElement;
|
||||||
private selectedEntries: Set<number> = new Set();
|
private selectedEntries: Set<number> = new Set();
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf, plugin: SubstackClipperPlugin) {
|
constructor(leaf: WorkspaceLeaf, plugin: SubstackClipperPlugin) {
|
||||||
|
|
@ -33,31 +33,49 @@ export class HistoryView extends ItemView {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.addClass('substack-clipper-history-view');
|
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',
|
type: 'text',
|
||||||
placeholder: 'Filter by title or author...',
|
|
||||||
cls: 'substack-clipper-history-search',
|
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', () => {
|
searchInput.addEventListener('input', () => {
|
||||||
this.filterText = searchInput.value.toLowerCase();
|
this.filterText = searchInput.value.toLowerCase();
|
||||||
|
clearBtn.toggleClass('is-visible', searchInput.value.length > 0);
|
||||||
this.renderList();
|
this.renderList();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listEl = contentEl.createDiv({ cls: 'substack-clipper-history-list' });
|
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 footer = contentEl.createDiv({ cls: 'substack-clipper-history-footer' });
|
||||||
const btn = footer.createEl('button', {
|
this.redownloadBtn = footer.createEl('button', {
|
||||||
text: 'Re-download comments',
|
text: 'Re-download comments',
|
||||||
cls: 'mod-cta',
|
cls: 'mod-cta',
|
||||||
|
attr: { 'aria-label': 'Re-download comments for selected entries' },
|
||||||
});
|
});
|
||||||
btn.setAttribute('aria-label', 'Re-download comments for selected entries');
|
this.redownloadBtn.disabled = true;
|
||||||
btn.addEventListener('click', () => {
|
this.redownloadBtn.addEventListener('click', () => {
|
||||||
void this.redownloadComments(btn);
|
void this.redownloadComments();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderList();
|
this.renderList();
|
||||||
|
|
@ -80,9 +98,14 @@ export class HistoryView extends ItemView {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private syncRedownloadBtn(): void {
|
||||||
|
this.redownloadBtn.disabled = this.selectedEntries.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
private renderList(): void {
|
private renderList(): void {
|
||||||
this.listEl.empty();
|
this.listEl.empty();
|
||||||
this.selectedEntries.clear();
|
this.selectedEntries.clear();
|
||||||
|
this.syncRedownloadBtn();
|
||||||
|
|
||||||
const entries = this.getFilteredEntries();
|
const entries = this.getFilteredEntries();
|
||||||
|
|
||||||
|
|
@ -99,7 +122,7 @@ export class HistoryView extends ItemView {
|
||||||
const header = this.listEl.createDiv({ cls: 'substack-clipper-history-header' });
|
const header = this.listEl.createDiv({ cls: 'substack-clipper-history-header' });
|
||||||
const selectAllCb = header.createEl('input', { type: 'checkbox' });
|
const selectAllCb = header.createEl('input', { type: 'checkbox' });
|
||||||
selectAllCb.setAttribute('aria-label', 'Select all entries');
|
selectAllCb.setAttribute('aria-label', 'Select all entries');
|
||||||
header.createEl('span', {
|
header.createSpan({
|
||||||
text: `${String(entries.length)} ${entries.length === 1 ? 'entry' : 'entries'}`,
|
text: `${String(entries.length)} ${entries.length === 1 ? 'entry' : 'entries'}`,
|
||||||
cls: 'substack-clipper-history-count',
|
cls: 'substack-clipper-history-count',
|
||||||
});
|
});
|
||||||
|
|
@ -124,22 +147,23 @@ export class HistoryView extends ItemView {
|
||||||
const checked = rowCheckboxes.filter(r => r.checkbox.checked).length;
|
const checked = rowCheckboxes.filter(r => r.checkbox.checked).length;
|
||||||
selectAllCb.checked = checked === total;
|
selectAllCb.checked = checked === total;
|
||||||
selectAllCb.indeterminate = checked > 0 && checked < total;
|
selectAllCb.indeterminate = checked > 0 && checked < total;
|
||||||
|
this.syncRedownloadBtn();
|
||||||
});
|
});
|
||||||
|
|
||||||
const info = row.createDiv({ cls: 'substack-clipper-history-info' });
|
const info = row.createDiv({ cls: 'substack-clipper-history-info' });
|
||||||
info.createEl('span', { text: entry.title, cls: 'substack-clipper-history-title' });
|
info.createSpan({ text: entry.title, cls: 'substack-clipper-history-title' });
|
||||||
|
|
||||||
const meta = info.createDiv({ cls: 'substack-clipper-history-meta' });
|
const meta = info.createDiv({ cls: 'substack-clipper-history-meta' });
|
||||||
const userLine = meta.createDiv({ cls: 'substack-clipper-history-userline' });
|
const userLine = meta.createDiv({ cls: 'substack-clipper-history-userline' });
|
||||||
userLine.createEl('span', { text: entry.username, cls: 'substack-clipper-history-username' });
|
userLine.createSpan({ text: entry.username, cls: 'substack-clipper-history-username' });
|
||||||
if (entry.commentCount > 0) {
|
if (entry.commentCount > 0) {
|
||||||
userLine.createEl('span', {
|
userLine.createSpan({
|
||||||
text: String(entry.commentCount),
|
text: String(entry.commentCount),
|
||||||
cls: 'substack-clipper-history-badge',
|
cls: 'substack-clipper-history-badge',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
meta.createEl('span', {
|
meta.createSpan({
|
||||||
text: new Date(entry.dateSaved).toLocaleDateString(),
|
text: new Date(entry.dateSaved).toLocaleDateString(),
|
||||||
cls: 'substack-clipper-history-date',
|
cls: 'substack-clipper-history-date',
|
||||||
});
|
});
|
||||||
|
|
@ -191,17 +215,27 @@ export class HistoryView extends ItemView {
|
||||||
this.selectedEntries.delete(idx);
|
this.selectedEntries.delete(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.syncRedownloadBtn();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async redownloadComments(btn: HTMLButtonElement): Promise<void> {
|
private setViewDisabled(disabled: boolean): void {
|
||||||
|
this.contentEl.toggleClass('is-disabled', disabled);
|
||||||
|
const inputs = Array.from(this.contentEl.querySelectorAll<HTMLInputElement | HTMLButtonElement>('input, button'));
|
||||||
|
for (const el of inputs) {
|
||||||
|
el.disabled = disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async redownloadComments(): Promise<void> {
|
||||||
if (this.selectedEntries.size === 0) {
|
if (this.selectedEntries.size === 0) {
|
||||||
new Notice('No entries selected.');
|
new Notice('No entries selected.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
this.setViewDisabled(true);
|
||||||
this.statusEl.setText('Downloading...');
|
const notice = new Notice('Re-downloading comments...', 0);
|
||||||
|
notice.containerEl.addClass('is-loading');
|
||||||
|
|
||||||
const history = this.plugin.settings.history;
|
const history = this.plugin.settings.history;
|
||||||
const indices = [...this.selectedEntries];
|
const indices = [...this.selectedEntries];
|
||||||
|
|
@ -222,7 +256,22 @@ export class HistoryView extends ItemView {
|
||||||
`${this.plugin.settings.saveDirectory}/${entry.username}/${entry.slug}-comments.md`,
|
`${this.plugin.settings.saveDirectory}/${entry.username}/${entry.slug}-comments.md`,
|
||||||
);
|
);
|
||||||
await this.plugin.writeFile(commentsPath, commentsMd);
|
await this.plugin.writeFile(commentsPath, commentsMd);
|
||||||
entry.commentCount = commentsData.comments.length;
|
entry.commentCount = countAllComments(commentsData);
|
||||||
|
|
||||||
|
const notePath = normalizePath(
|
||||||
|
`${this.plugin.settings.saveDirectory}/${entry.username}/${entry.slug}.md`,
|
||||||
|
);
|
||||||
|
const noteFile = this.app.vault.getAbstractFileByPath(notePath);
|
||||||
|
if (noteFile instanceof TFile) {
|
||||||
|
const content = await this.app.vault.read(noteFile);
|
||||||
|
const updated = content.replace(
|
||||||
|
/^comment-count: \d+$/m,
|
||||||
|
`comment-count: ${String(entry.commentCount)}`,
|
||||||
|
);
|
||||||
|
if (updated !== content) {
|
||||||
|
await this.app.vault.modify(noteFile, updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry.lastUpdated = new Date().toISOString();
|
entry.lastUpdated = new Date().toISOString();
|
||||||
succeeded++;
|
succeeded++;
|
||||||
|
|
@ -233,18 +282,15 @@ export class HistoryView extends ItemView {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
notice.hide();
|
||||||
|
this.setViewDisabled(false);
|
||||||
this.renderList();
|
this.renderList();
|
||||||
btn.disabled = false;
|
|
||||||
|
|
||||||
if (failed === 0) {
|
if (failed === 0) {
|
||||||
const msg = `Re-downloaded comments for ${String(succeeded)} posts`;
|
new Notice(`Re-downloaded comments for ${String(succeeded)} posts`);
|
||||||
this.statusEl.setText(msg);
|
|
||||||
new Notice(msg);
|
|
||||||
} else {
|
} else {
|
||||||
const total = succeeded + failed;
|
const total = succeeded + failed;
|
||||||
const msg = `Re-downloaded ${String(succeeded)} of ${String(total)} (${String(failed)} failed)`;
|
new Notice(`Re-downloaded ${String(succeeded)} of ${String(total)} (${String(failed)} failed)`);
|
||||||
this.statusEl.setText(msg);
|
|
||||||
new Notice(msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/main.ts
16
src/main.ts
|
|
@ -8,7 +8,7 @@ import { fetchHtml, extractPreloads, parsePost, parseUrl, extractImages, extract
|
||||||
import { htmlToMarkdown } from './converter';
|
import { htmlToMarkdown } from './converter';
|
||||||
import { postprocessMarkdown } from './postprocess';
|
import { postprocessMarkdown } from './postprocess';
|
||||||
import { downloadAllMedia } from './downloader';
|
import { downloadAllMedia } from './downloader';
|
||||||
import { fetchComments, renderComments } from './comments';
|
import { countAllComments, fetchComments, renderComments } from './comments';
|
||||||
|
|
||||||
export default class SubstackClipperPlugin extends Plugin {
|
export default class SubstackClipperPlugin extends Plugin {
|
||||||
settings: SubstackClipperSettings;
|
settings: SubstackClipperSettings;
|
||||||
|
|
@ -40,6 +40,18 @@ export default class SubstackClipperPlugin extends Plugin {
|
||||||
void this.activateHistoryView();
|
void this.activateHistoryView();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.registerObsidianProtocolHandler('substack-clipper', (params) => {
|
||||||
|
const url = params.url;
|
||||||
|
if (!url || !url.includes('/p/')) {
|
||||||
|
new Notice('Invalid substack-clipper uri — a "URL" parameter containing /p/ is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const openAfterClip = params.open === 'true' ? true
|
||||||
|
: params.open === 'false' ? false
|
||||||
|
: this.settings.openAfterClip;
|
||||||
|
void this.clipPost(url, openAfterClip);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async activateHistoryView(): Promise<void> {
|
async activateHistoryView(): Promise<void> {
|
||||||
|
|
@ -64,6 +76,7 @@ export default class SubstackClipperPlugin extends Plugin {
|
||||||
|
|
||||||
private async clipPost(url: string, openAfterClip: boolean): Promise<void> {
|
private async clipPost(url: string, openAfterClip: boolean): Promise<void> {
|
||||||
const notice = new Notice('Saving...', 0);
|
const notice = new Notice('Saving...', 0);
|
||||||
|
notice.containerEl.addClass("is-loading");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { username, slug, domain } = parseUrl(url);
|
const { username, slug, domain } = parseUrl(url);
|
||||||
|
|
@ -114,6 +127,7 @@ export default class SubstackClipperPlugin extends Plugin {
|
||||||
await this.writeFile(commentsJsonPath, JSON.stringify(commentsData, null, 2));
|
await this.writeFile(commentsJsonPath, JSON.stringify(commentsData, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
article.commentCount = countAllComments(commentsData);
|
||||||
const commentsMd = renderComments(commentsData, slug, domain);
|
const commentsMd = renderComments(commentsData, slug, domain);
|
||||||
const commentsPath = normalizePath(`${this.settings.saveDirectory}/${username}/${slug}-comments.md`);
|
const commentsPath = normalizePath(`${this.settings.saveDirectory}/${username}/${slug}-comments.md`);
|
||||||
await this.writeFile(commentsPath, commentsMd);
|
await this.writeFile(commentsPath, commentsMd);
|
||||||
|
|
|
||||||
98
styles.css
98
styles.css
|
|
@ -19,16 +19,96 @@
|
||||||
gap: var(--size-4-2);
|
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 {
|
.substack-clipper-history-search {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
padding: var(--size-4-2);
|
min-width: 0;
|
||||||
border: 1px solid var(--background-modifier-border);
|
padding: var(--size-4-1) 0;
|
||||||
border-radius: var(--radius-s);
|
border: none;
|
||||||
background: var(--background-primary);
|
border-radius: 0;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
font-size: var(--font-ui-medium);
|
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 {
|
.substack-clipper-history-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -128,11 +208,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.substack-clipper-history-status {
|
.substack-clipper-history-view.is-disabled {
|
||||||
padding: var(--size-4-2) 0;
|
pointer-events: none;
|
||||||
color: var(--text-muted);
|
opacity: 0.5;
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
min-height: var(--size-4-4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.substack-clipper-history-footer {
|
.substack-clipper-history-footer {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES5",
|
"ES5",
|
||||||
"ES6",
|
"ES6",
|
||||||
"ES7"
|
"ES7",
|
||||||
|
"ES2017.String",
|
||||||
|
"ES2020.Promise"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,12 @@
|
||||||
"0.1.2": "1.13.0",
|
"0.1.2": "1.13.0",
|
||||||
"0.1.3": "1.13.0",
|
"0.1.3": "1.13.0",
|
||||||
"0.1.4": "1.13.0",
|
"0.1.4": "1.13.0",
|
||||||
"0.1.5": "1.13.0"
|
"0.1.5": "1.13.0",
|
||||||
|
"0.1.6": "1.13.0",
|
||||||
|
"0.1.7": "1.13.0",
|
||||||
|
"0.1.8": "1.13.0",
|
||||||
|
"0.1.9": "1.13.0",
|
||||||
|
"0.1.10": "1.13.0",
|
||||||
|
"0.1.11": "1.13.0",
|
||||||
|
"0.1.12": "1.13.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue