diff --git a/src/ClipperCatalog.tsx b/src/ClipperCatalog.tsx index 9d9e03b..0eb9340 100644 --- a/src/ClipperCatalog.tsx +++ b/src/ClipperCatalog.tsx @@ -19,6 +19,7 @@ interface Article { contentTags: string[]; basename: string; content: string; + read?: boolean; } interface SortConfig { @@ -146,7 +147,8 @@ const ClipperCatalog: React.FC = ({ app, plugin }) => { if (metadata?.frontmatter) { const source = metadata.frontmatter[plugin.settings.sourcePropertyName]; - + const read = metadata.frontmatter[plugin.settings.readPropertyName] === true; + if (source) { const content = await app.vault.read(file); let frontmatterTags: string[] = []; @@ -191,7 +193,8 @@ const ClipperCatalog: React.FC = ({ app, plugin }) => { frontmatterTags, contentTags, basename: file.basename, - content: content + content: content, + read }); } } @@ -508,14 +511,22 @@ const ClipperCatalog: React.FC = ({ app, plugin }) => {
+ {plugin.settings.readPropertyName && ( + + )} - - - + + + + {plugin.settings.readPropertyName && ( + + )} {filteredArticles.map((article) => ( + {plugin.settings.readPropertyName && ( + + )}
+ {/* Read */} + handleSort('title')} className="cc-px-4 cc-py-2 cc-text-left cc-cursor-pointer clipper-catalog-header-cell" @@ -558,10 +569,49 @@ const ClipperCatalog: React.FC = ({ app, plugin }) => {
e.stopPropagation()}> + { + const isChecked = e.target.checked; + const file = app.vault.getAbstractFileByPath(article.path); + if (!(file instanceof TFile)) return; + + try { + setArticles(prev => prev.map(a => + a.path === article.path ? {...a, read: isChecked} : a + )); + + const metadata = app.metadataCache.getFileCache(file); + const content = await app.vault.read(file); + + if (!metadata?.frontmatter) { + const newContent = `---\n${plugin.settings.readPropertyName}: ${isChecked}\n---\n${content}`; + await app.vault.modify(file, newContent); + } else { + await app.fileManager.processFrontMatter(file, (frontmatter) => { + frontmatter[plugin.settings.readPropertyName] = isChecked; + }); + } + } catch (error) { + console.error('Error:', error); + setArticles(prev => prev.map(a => + a.path === article.path ? {...a, read: !isChecked} : a + )); + } + }} + /> + openArticle(article.path, event)} - className="cc-flex cc-items-center cc-gap-2 cc-cursor-pointer cc-transition-colors cc-min-h-[1.5rem] clipper-catalog-title" + className={`cc-flex cc-items-center cc-gap-2 cc-cursor-pointer cc-transition-colors cc-min-h-[1.5rem] clipper-catalog-title ${ + (plugin.settings.readPropertyName && !article.read) ? 'cc-font-bold' : '' + }`} > = ({ app, plugin }) => { > + {(() => { const abstractFile = app.vault.getAbstractFileByPath(article.path); if (abstractFile instanceof TFile) { diff --git a/src/main.tsx b/src/main.tsx index ae67fbb..6b7768d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,6 +9,8 @@ interface ObsidianClipperCatalogSettings { isAdvancedSettingsExpanded: boolean; includeFrontmatterTags: boolean; openInSameLeaf: boolean; + readPropertyName: string; + useNativeCheckbox: boolean; } interface ObsidianSettings { @@ -24,7 +26,9 @@ const DEFAULT_SETTINGS: ObsidianClipperCatalogSettings = { ignoredDirectories: [], isAdvancedSettingsExpanded: false, includeFrontmatterTags: true, - openInSameLeaf: false + openInSameLeaf: false, + readPropertyName: '', + useNativeCheckbox: false } export default class ObsidianClipperCatalog extends Plugin { @@ -121,14 +125,21 @@ class ClipperCatalogSettingTab extends PluginSettingTab { })); new Setting(containerEl) - .setName('Include frontmatter tags') - .setDesc('Include tags from the frontmatter "tags" field') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.includeFrontmatterTags) - .onChange(async (value) => { - this.plugin.settings.includeFrontmatterTags = value; - await this.plugin.saveSettings(); - })); + .setName('Include frontmatter tags') + .setDesc('Include tags from the frontmatter "tags" field') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.includeFrontmatterTags) + .onChange(async (value) => { + this.plugin.settings.includeFrontmatterTags = value; + await this.plugin.saveSettings(); + // Refresh all articles + this.app.workspace.getLeavesOfType(VIEW_TYPE_CLIPPER_CATALOG).forEach(leaf => { + if (leaf.view instanceof ClipperCatalogView) { + // Force reload articles + leaf.view.onOpen(); + } + }); + })); new Setting(containerEl) .setName('Open notes in same window') @@ -139,5 +150,37 @@ class ClipperCatalogSettingTab extends PluginSettingTab { this.plugin.settings.openInSameLeaf = value; await this.plugin.saveSettings(); })); - } + + containerEl.createEl('div', { + text: '⚠️ Warning: The next setting will allow the catalog to overwrite any property you set here. Proceed if you know what you are doing.', + cls: 'setting-item-description warning-text' + }).style.color = 'var(--text-warning)'; + + new Setting(containerEl) + .setName('Read status property name') + .setDesc('Leave blank to hide checkboxes. If set, specifies which frontmatter property tracks read status (e.g., "read", "completed"). Note: Changing this affects new changes only and will not erase any values.') + .addText(text => text + .setPlaceholder('e.g., read') + .setValue(this.plugin.settings.readPropertyName) + .onChange(async (value) => { + this.plugin.settings.readPropertyName = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Use a compatible checkbox') + .setDesc('Experimental: Enable for better compatibility with themes where the custom checkbox is not visible.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.useNativeCheckbox) + .onChange(async (value) => { + this.plugin.settings.useNativeCheckbox = value; + await this.plugin.saveSettings(); + // Refresh all clipper catalog views + this.app.workspace.getLeavesOfType(VIEW_TYPE_CLIPPER_CATALOG).forEach(leaf => { + if (leaf.view) { + leaf.view.onResize(); + } + }); + })); + } } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index ae0653f..cfc9244 100644 --- a/src/styles.css +++ b/src/styles.css @@ -190,9 +190,73 @@ .clipper-catalog-title { color: var(--text-normal); + .cc-font-bold { + font-weight: 600; + } +} + +/* Checkbox styles */ +.clipper-catalog-checkbox { + appearance: none; + -webkit-appearance: none; + background-color: var(--background-primary); + border: 1px solid var(--checkbox-border-color, var(--text-muted)); + border-radius: 4px; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + margin: 0; + padding: 0; +} + +.clipper-catalog-checkbox:checked { + background-color: var(--interactive-accent); + border-color: var(--interactive-accent); +} + +.clipper-catalog-checkbox:checked::before { + content: ""; + width: 10px; + height: 10px; + background-color: var(--text-on-accent, white); + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); +} + +/* Compatibility for themes */ +/* Dark Theme */ +.theme-dark{ + input.clipper-catalog-checkbox[type=checkbox]:checked:after{ + background-color: transparent; + } +} + +/* Compatible checkbox enhancements */ +.clipper-catalog-compatible-checkbox { + width: 16px; + height: 16px; + cursor: pointer; + border: 1px solid var(--checkbox-border-color); + border-radius: 3px; + background-color: var(--background-primary); + vertical-align: middle; + padding: 0; + margin: 0; +} + +.clipper-catalog-compatible-checkbox:checked { + background-color: var(--interactive-accent); + border-color: var(--interactive-accent); +} + +input.clipper-catalog-compatible-checkbox[type=checkbox]:checked { + /* --checkbox-color: var(--checkbox-color); */ + background-color: var(--interactive-accent); } -/* Tag styles */ /* Tag styles */ .clipper-catalog-tag, .clipper-catalog-frontmatter-tag { @@ -328,4 +392,4 @@ max-width: 150px; font-size: 10px; } -} \ No newline at end of file +} diff --git a/styles.css b/styles.css index d3b0b9e..c8c92e7 100644 --- a/styles.css +++ b/styles.css @@ -87,8 +87,8 @@ width: 1rem; } -.cc-w-\[13\%\] { - width: 13%; +.cc-w-\[10\%\] { + width: 10%; } .cc-w-\[15\%\] { @@ -103,6 +103,10 @@ width: 22%; } +.cc-w-\[3\%\] { + width: 3%; +} + .cc-w-\[30\%\] { width: 30%; } @@ -301,6 +305,10 @@ line-height: 1rem; } +.cc-font-bold { + font-weight: 700; +} + .cc-font-medium { font-weight: 500; } @@ -573,9 +581,77 @@ .clipper-catalog-title { color: var(--text-normal); + .cc-font-bold { + font-weight: 600; + } } -/* Tag styles */ +/* Checkbox styles */ + +.clipper-catalog-checkbox { + -moz-appearance: none; + appearance: none; + -webkit-appearance: none; + background-color: var(--background-primary); + border: 1px solid var(--checkbox-border-color, var(--text-muted)); + border-radius: 4px; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + margin: 0; + padding: 0; +} + +.clipper-catalog-checkbox:checked { + background-color: var(--interactive-accent); + border-color: var(--interactive-accent); +} + +.clipper-catalog-checkbox:checked::before { + content: ""; + width: 10px; + height: 10px; + background-color: var(--text-on-accent, white); + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); +} + +/* Compatibility for themes */ + +/* Dark Theme */ + +.theme-dark{ + input.clipper-catalog-checkbox[type=checkbox]:checked:after{ + background-color: transparent; + } +} + +/* Compatible checkbox enhancements */ + +.clipper-catalog-compatible-checkbox { + width: 16px; + height: 16px; + cursor: pointer; + border: 1px solid var(--checkbox-border-color); + border-radius: 3px; + background-color: var(--background-primary); + vertical-align: middle; + padding: 0; + margin: 0; +} + +.clipper-catalog-compatible-checkbox:checked { + background-color: var(--interactive-accent); + border-color: var(--interactive-accent); +} + +input.clipper-catalog-compatible-checkbox[type=checkbox]:checked { + /* --checkbox-color: var(--checkbox-color); */ + background-color: var(--interactive-accent); +} /* Tag styles */ @@ -807,4 +883,4 @@ .dark\:hover\:cc-text-gray-300:hover:is(.cc-dark *) { --tw-text-opacity: 1; color: rgb(209 213 219 / var(--tw-text-opacity, 1)); -} \ No newline at end of file +}