mirror of
https://github.com/soundslikeinfo/obsidian-clipper-catalog.git
synced 2026-07-22 11:30:32 +00:00
Merge pull request #16 from soundslikeinfo/8-read-checkbox
8 read checkbox
This commit is contained in:
commit
3de7cf2052
4 changed files with 256 additions and 22 deletions
|
|
@ -19,6 +19,7 @@ interface Article {
|
|||
contentTags: string[];
|
||||
basename: string;
|
||||
content: string;
|
||||
read?: boolean;
|
||||
}
|
||||
|
||||
interface SortConfig {
|
||||
|
|
@ -146,7 +147,8 @@ const ClipperCatalog: React.FC<ClipperCatalogProps> = ({ 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<ClipperCatalogProps> = ({ app, plugin }) => {
|
|||
frontmatterTags,
|
||||
contentTags,
|
||||
basename: file.basename,
|
||||
content: content
|
||||
content: content,
|
||||
read
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -508,14 +511,22 @@ const ClipperCatalog: React.FC<ClipperCatalogProps> = ({ app, plugin }) => {
|
|||
<div className="cc-overflow-x-auto cc-min-h-[120px]">
|
||||
<table className="cc-w-full cc-text-sm">
|
||||
<colgroup>
|
||||
{plugin.settings.readPropertyName && (
|
||||
<col className="cc-w-[3%]" />
|
||||
)}
|
||||
<col className="cc-w-[30%]" />
|
||||
<col className="cc-w-[15%] cc-narrow-view-hidden" />
|
||||
<col className="cc-w-[22%] cc-narrow-view-hidden" />
|
||||
<col className="cc-w-[20%]" />
|
||||
<col className="cc-w-[13%]" />
|
||||
<col className="cc-w-[20%] cc-narrow-view-hidden" />
|
||||
<col className="cc-w-[22%]" />
|
||||
<col className="cc-w-[10%]" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="clipper-catalog-header-row">
|
||||
{plugin.settings.readPropertyName && (
|
||||
<th className="cc-px-4 cc-py-2 cc-text-left clipper-catalog-header-cell">
|
||||
{/* Read */}
|
||||
</th>
|
||||
)}
|
||||
<th
|
||||
onClick={() => 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<ClipperCatalogProps> = ({ app, plugin }) => {
|
|||
<tbody>
|
||||
{filteredArticles.map((article) => (
|
||||
<tr key={article.path} className="clipper-catalog-row">
|
||||
{plugin.settings.readPropertyName && (
|
||||
<td className="cc-px-4 cc-py-2" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={plugin.settings.useNativeCheckbox ? 'clipper-catalog-compatible-checkbox' : 'clipper-catalog-checkbox'}
|
||||
checked={article.read === true}
|
||||
onChange={async (e) => {
|
||||
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
|
||||
));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className="cc-px-4 cc-py-2">
|
||||
<span
|
||||
onClick={(event) => 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' : ''
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className="cc-h-4 cc-w-4 cc-flex-shrink-0 clipper-catalog-icon"
|
||||
|
|
@ -572,6 +622,7 @@ const ClipperCatalog: React.FC<ClipperCatalogProps> = ({ app, plugin }) => {
|
|||
>
|
||||
<path d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
|
||||
{(() => {
|
||||
const abstractFile = app.vault.getAbstractFileByPath(article.path);
|
||||
if (abstractFile instanceof TFile) {
|
||||
|
|
|
|||
63
src/main.tsx
63
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();
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
styles.css
84
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue