mirror of
https://github.com/travisvn/obsidian-vision-recall.git
synced 2026-07-22 05:38:13 +00:00
Refactor and modernize project structure and styling
- Migrated CSS files to SCSS with more modular and scoped styling - Added new DeleteConfirmationModal for consistent deletion UX - Updated file and folder handling to use Obsidian's native methods - Improved local storage interactions through Obsidian app methods - Bumped version to 1.0.4 and updated minimum app version - Added new path alias and TypeScript configuration updates And all other points outlined in the PR review
This commit is contained in:
parent
1705c01c15
commit
a417a7cb21
45 changed files with 6633 additions and 6182 deletions
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "vision-recall",
|
||||
"name": "Vision Recall",
|
||||
"version": "1.0.3",
|
||||
"minAppVersion": "0.15.0",
|
||||
"version": "1.0.4",
|
||||
"minAppVersion": "1.8.3",
|
||||
"description": "Transform screenshots into searchable notes using AI vision and text analysis.",
|
||||
"author": "Travis Van Nimwegen",
|
||||
"authorUrl": "https://travis.engineer",
|
||||
|
|
|
|||
1009
package-lock.json
generated
1009
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-vision-recall",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": "Transform screenshots into searchable Obsidian notes using AI vision and text analysis.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"postcss": "^8.5.1",
|
||||
"sass": "^1.85.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "^5.7.3",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const InitializeFolders = ({ foldersInitialized, setFoldersInitialized }:
|
|||
if (success) {
|
||||
setFoldersInitialized(true);
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.INITIALIZE_FOLDERS, 'true');
|
||||
plugin.app.saveLocalStorage(STORAGE_KEYS.INITIALIZE_FOLDERS, 'true');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const ConfigModalContent = ({
|
|||
useEffect(() => {
|
||||
if (initialized) return;
|
||||
const loadConfig = async () => {
|
||||
const currentConfig = await dataManager.getConfig();
|
||||
const currentConfig = dataManager.getConfig();
|
||||
setConfig(currentConfig);
|
||||
setInitialized(true);
|
||||
};
|
||||
|
|
@ -42,7 +42,7 @@ const ConfigModalContent = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="config-modal-container">
|
||||
<div className="vr config-modal-container">
|
||||
<form onSubmit={handleSubmit} className="config-modal-form">
|
||||
|
||||
<div className="config-field">
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const DebugOperationsView = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="config-modal-container">
|
||||
<div className="vr config-modal-container">
|
||||
<div className="debug-operations-content">
|
||||
<button
|
||||
className="mod-warning cursor-pointer"
|
||||
|
|
|
|||
68
src/components/modals/DeleteConfirmationModal.tsx
Normal file
68
src/components/modals/DeleteConfirmationModal.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { App, Modal } from 'obsidian';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import VisionRecallPlugin from '@/main';
|
||||
|
||||
interface DeleteConfirmationModalProps {
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DeleteConfirmationForm: React.FC<DeleteConfirmationModalProps> = ({ message, onConfirm, onClose }) => {
|
||||
return (
|
||||
<div className="vr vr-delete-confirmation-modal">
|
||||
<div className="message">{message}</div>
|
||||
|
||||
<div className="button-group">
|
||||
<button type="button" onClick={onClose}>Close</button>
|
||||
<button type="button" onClick={onConfirm}>Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export class DeleteConfirmationModal extends Modal {
|
||||
private message: string;
|
||||
private handleConfirm: () => Promise<void>;
|
||||
private handleClose: () => void;
|
||||
private plugin: VisionRecallPlugin;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: VisionRecallPlugin,
|
||||
message: string,
|
||||
onConfirm: () => Promise<void>,
|
||||
onClose?: () => void
|
||||
) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
this.handleConfirm = onConfirm;
|
||||
this.handleClose = onClose;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
async handleConfirmWrapper() {
|
||||
await this.handleConfirm();
|
||||
this.close();
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl, titleEl } = this;
|
||||
titleEl.setText('Confirm deletion');
|
||||
const root = createRoot(contentEl);
|
||||
|
||||
root.render(
|
||||
<DeleteConfirmationForm
|
||||
message={this.message}
|
||||
onConfirm={() => this.handleConfirmWrapper()}
|
||||
onClose={() => this.handleClose()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +1,48 @@
|
|||
import { App, MarkdownRenderer, Modal } from 'obsidian';
|
||||
import { App, MarkdownRenderer, Modal, requestUrl } from 'obsidian';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import VisionRecallPlugin from '@/main';
|
||||
|
||||
interface DocViewerFormProps {
|
||||
docPath: string;
|
||||
onClose: () => void;
|
||||
app: App;
|
||||
plugin: VisionRecallPlugin;
|
||||
}
|
||||
|
||||
const DOC_PATHS = {
|
||||
OLLAMA_SETUP: 'ollama-setup.md',
|
||||
REFERENCE_GUIDE: 'reference.md',
|
||||
const DOC_LOCATIONS = {
|
||||
'ollama-setup': 'https://raw.githubusercontent.com/travisvn/obsidian-vision-recall/main/docs/ollama-setup.md',
|
||||
'reference-guide': 'https://raw.githubusercontent.com/travisvn/obsidian-vision-recall/main/docs/reference.md',
|
||||
}
|
||||
|
||||
const DocViewerForm: React.FC<DocViewerFormProps> = ({ docPath, onClose, app, plugin }) => {
|
||||
const DocViewerForm: React.FC<DocViewerFormProps> = ({ onClose, app, plugin }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedDoc, setSelectedDoc] = useState<string>('ollama-setup');
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadDocument = async () => {
|
||||
const fetchContent = async (url: string) => {
|
||||
try {
|
||||
const fileContent = await app.vault.adapter.read(docPath);
|
||||
setContent(fileContent);
|
||||
} catch (err) {
|
||||
console.error('Error loading document:', err);
|
||||
setError('Failed to load document');
|
||||
// const response = await fetch(url);
|
||||
const response = await requestUrl(url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const text = response.text;
|
||||
return text;
|
||||
} catch (error) {
|
||||
console.error('Error fetching content:', error);
|
||||
setError('Failed to fetch content');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadDocument();
|
||||
}, [docPath, app.vault]);
|
||||
useEffect(() => {
|
||||
const fetchContentEffect = async () => {
|
||||
const content = await fetchContent(DOC_LOCATIONS[selectedDoc]);
|
||||
setContent(content);
|
||||
}
|
||||
fetchContentEffect();
|
||||
}, [selectedDoc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !content) return;
|
||||
|
|
@ -46,15 +56,13 @@ const DocViewerForm: React.FC<DocViewerFormProps> = ({ docPath, onClose, app, pl
|
|||
}, [content, app, plugin]);
|
||||
|
||||
return (
|
||||
<div className="doc-viewer-modal">
|
||||
<div className="vr doc-viewer-modal">
|
||||
<div className='flex flex-row gap-2 justify-center items-center'>
|
||||
<button
|
||||
type='button'
|
||||
className='cursor-pointer'
|
||||
onClick={async () => {
|
||||
const newDocPath = `${plugin.manifest.dir}/docs/${DOC_PATHS.OLLAMA_SETUP}`;
|
||||
const fileContent = await app.vault.adapter.read(newDocPath);
|
||||
setContent(fileContent);
|
||||
setSelectedDoc('ollama-setup');
|
||||
}}
|
||||
>
|
||||
Ollama setup
|
||||
|
|
@ -63,9 +71,7 @@ const DocViewerForm: React.FC<DocViewerFormProps> = ({ docPath, onClose, app, pl
|
|||
type='button'
|
||||
className='cursor-pointer'
|
||||
onClick={async () => {
|
||||
const newDocPath = `${plugin.manifest.dir}/docs/${DOC_PATHS.REFERENCE_GUIDE}`;
|
||||
const fileContent = await app.vault.adapter.read(newDocPath);
|
||||
setContent(fileContent);
|
||||
setSelectedDoc('reference-guide');
|
||||
}}
|
||||
>
|
||||
Reference guide
|
||||
|
|
@ -81,13 +87,11 @@ const DocViewerForm: React.FC<DocViewerFormProps> = ({ docPath, onClose, app, pl
|
|||
};
|
||||
|
||||
export class DocViewerModal extends Modal {
|
||||
private docPath: string;
|
||||
private plugin: VisionRecallPlugin;
|
||||
|
||||
constructor(app: App, plugin: VisionRecallPlugin, docName: string) {
|
||||
constructor(app: App, plugin: VisionRecallPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.docPath = `${plugin.manifest.dir}/docs/${docName}.md`;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
|
|
@ -97,7 +101,6 @@ export class DocViewerModal extends Modal {
|
|||
|
||||
root.render(
|
||||
<DocViewerForm
|
||||
docPath={this.docPath}
|
||||
onClose={() => this.close()}
|
||||
app={this.app}
|
||||
plugin={this.plugin}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const EditMetadataForm: React.FC<EditMetadataModalProps> = ({ metadata, onSave,
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="edit-metadata-modal">
|
||||
<div className="vr vr-edit-metadata-modal">
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const FileUploadView = (props: FileUploadViewProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="file-upload-modal-content">
|
||||
<div className="vr file-upload-modal-content">
|
||||
<div className="file-upload-container">
|
||||
<input
|
||||
type="file"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const QueueControls: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="queue-controls flex flex-row items-center gap-2 mb-4 mx-auto justify-center">
|
||||
<div className="vr queue-controls flex flex-row items-center gap-2 mb-4 mx-auto justify-center">
|
||||
{shouldShowControls ? (
|
||||
<>
|
||||
{status.isProcessing ? (
|
||||
|
|
@ -77,8 +77,8 @@ const QueueStatus: React.FC = () => {
|
|||
const { status } = useQueueStore();
|
||||
|
||||
return (
|
||||
<div className="queue-status mb-4 mx-auto justify-center">
|
||||
<div className="text-sm font-medium">
|
||||
<div className="vr queue-status mb-4 mx-auto justify-center w-full">
|
||||
<div className="text-sm font-medium text-center">
|
||||
{status.isProcessing ? (
|
||||
status.isPaused ? (
|
||||
<span className="text-yellow-500">⏸ Processing paused</span>
|
||||
|
|
@ -104,7 +104,7 @@ const ProcessingQueueView: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="processing-queue-modal w-full">
|
||||
<div className="vr processing-queue-modal w-full">
|
||||
|
||||
<div className="queue-stats flex flex-row items-center justify-between gap-1 w-full mb-4 p-2 bg-secondary rounded">
|
||||
<div>Total: {status.queue.length}</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class ScreenshotModal extends Modal {
|
|||
contentEl.addClass('screenshot-modal-content');
|
||||
|
||||
// Create a link element
|
||||
const linkEl = contentEl.createEl('a', { cls: 'screenshot-modal-link' });
|
||||
const linkEl = contentEl.createEl('a', { cls: 'vr screenshot-modal-link' });
|
||||
|
||||
// Set the link to open the image in Obsidian
|
||||
const file = this.app.vault.getAbstractFileByPath(this.imagePath);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const TestConfigView: React.FC<TestConfigModalProps> = ({ dataManager, plugin, o
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4 items-center justify-center'>
|
||||
<div className='flex flex-col gap-4 items-center justify-center vision-recall-styling'>
|
||||
<details className='w-full'>
|
||||
<summary
|
||||
aria-label='Expand LLM configuration'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface ViewMetadataModalProps {
|
|||
|
||||
const ViewMetadataForm: React.FC<ViewMetadataModalProps> = ({ metadata, onClose }) => {
|
||||
return (
|
||||
<div className="view-metadata-modal select-text">
|
||||
<div className="vr view-metadata-modal select-text">
|
||||
|
||||
<div className="metadata-section">
|
||||
<h4>File information</h4>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ interface DataContextType {
|
|||
addEntry: (entry: UserData) => Promise<void>;
|
||||
removeEntry: (id: string) => Promise<void>;
|
||||
refreshEntries: () => Promise<void>;
|
||||
getConfig: () => Promise<Config>;
|
||||
getConfigSynchronous: () => Config;
|
||||
getConfig: () => Config;
|
||||
setConfig: (config: Config) => Promise<void>;
|
||||
getAvailableTags: () => Set<string>;
|
||||
setAvailableTags: (tags: Set<string>) => Promise<void>;
|
||||
|
|
@ -64,12 +63,8 @@ export const DataProvider: React.FC<{ dataManager: DataManager; children: React.
|
|||
await refreshEntries(); // Ensure immediate UI update
|
||||
};
|
||||
|
||||
const getConfig = async () => {
|
||||
return await dataManager.getConfig();
|
||||
};
|
||||
|
||||
const getConfigSynchronous = () => {
|
||||
return dataManager.getConfigSynchronous();
|
||||
const getConfig = () => {
|
||||
return dataManager.getConfig();
|
||||
};
|
||||
|
||||
const setConfig = async (config: Config) => {
|
||||
|
|
@ -100,7 +95,6 @@ export const DataProvider: React.FC<{ dataManager: DataManager; children: React.
|
|||
removeEntry,
|
||||
refreshEntries,
|
||||
getConfig,
|
||||
getConfigSynchronous,
|
||||
setConfig,
|
||||
getAvailableTags,
|
||||
setAvailableTags,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import VisionRecallPlugin from '@/main';
|
||||
import { Low, Memory } from 'lowdb';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Events, normalizePath } from 'obsidian';
|
||||
import { Events, normalizePath, TFile } from 'obsidian';
|
||||
import { Config, DefaultConfig } from '@/types/config-types';
|
||||
import { ProcessedFileRecord } from '@/types/processing-types';
|
||||
import { customParse, customStringify } from '@/lib/json-utils';
|
||||
|
|
@ -82,15 +82,11 @@ const DEFAULT_STORED_DATA: StoredData = {
|
|||
export class DataManager extends Events {
|
||||
private db: Low<StoredData>;
|
||||
plugin: VisionRecallPlugin;
|
||||
private pollIntervalId: number | null = null;
|
||||
|
||||
constructor(plugin: VisionRecallPlugin) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.db = new Low(new Memory(), DEFAULT_STORED_DATA);
|
||||
|
||||
// Cleanup interval when plugin unloads
|
||||
plugin.register(() => this.stopIntakeDirectoryPolling());
|
||||
}
|
||||
|
||||
// Add a helper to check for valid config in the loaded data.
|
||||
|
|
@ -164,6 +160,7 @@ export class DataManager extends Events {
|
|||
|
||||
|
||||
/** Initialize and load user data, preserving other settings */
|
||||
/** Keeping for posterity sake as the application needs change */
|
||||
async initOLD() {
|
||||
try {
|
||||
const savedData = await this.plugin.loadData();
|
||||
|
|
@ -420,7 +417,7 @@ export class DataManager extends Events {
|
|||
|
||||
// Only create a new backup if one doesn't exist for today
|
||||
if (!this.plugin.app.vault.getAbstractFileByPath(newLocationForExistingFile)) {
|
||||
const currentFileContent = await this.plugin.app.vault.read(file);
|
||||
const currentFileContent = await this.plugin.app.vault.cachedRead(file);
|
||||
await this.plugin.app.vault.create(newLocationForExistingFile, currentFileContent);
|
||||
this.plugin.logger.info("created new backup file", newLocationForExistingFile);
|
||||
|
||||
|
|
@ -446,7 +443,9 @@ export class DataManager extends Events {
|
|||
}
|
||||
}
|
||||
|
||||
await this.plugin.app.vault.modify(file, dataStr);
|
||||
this.plugin.app.vault.process(file, (data: string) => {
|
||||
return dataStr;
|
||||
});
|
||||
this.plugin.logger.info("modified existing file", file.path);
|
||||
}
|
||||
}
|
||||
|
|
@ -477,14 +476,6 @@ export class DataManager extends Events {
|
|||
async importUserData(jsonData: string) {
|
||||
try {
|
||||
const parsedData = customParse(jsonData) as StoredData;
|
||||
|
||||
// Ensure the imported data has the right structure
|
||||
// if (!parsedData || typeof parsedData !== 'object' || !Array.isArray(parsedData.userData.list) || typeof parsedData.userData.map !== 'object') {
|
||||
// throw new Error('Invalid userData format.');
|
||||
// }
|
||||
|
||||
// Merge new user data with the existing database
|
||||
// this.db.data.userData = parsedData.userData;
|
||||
this.db.data = parsedData;
|
||||
await this.persist();
|
||||
} catch (error) {
|
||||
|
|
@ -493,23 +484,10 @@ export class DataManager extends Events {
|
|||
}
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
getConfig() {
|
||||
return this.db.data.config || {};
|
||||
}
|
||||
|
||||
getConfigSynchronous() {
|
||||
return this.db.data.config || {};
|
||||
}
|
||||
|
||||
async updateConfigOLD(updatedConfig: Partial<Config>) {
|
||||
this.db.data.config = {
|
||||
...this.db.data.config,
|
||||
...updatedConfig
|
||||
};
|
||||
await this.persist();
|
||||
this.trigger('config-updated'); // Emit config-updated event
|
||||
}
|
||||
|
||||
getProcessedFileRecords() {
|
||||
return this.db.data.processedFileRecords;
|
||||
}
|
||||
|
|
@ -573,30 +551,8 @@ export class DataManager extends Events {
|
|||
await this.persist();
|
||||
}
|
||||
|
||||
async startIntakeDirectoryPolling() {
|
||||
if (!this.db.data.config.enableIntakeFolderPolling) return;
|
||||
|
||||
// consider intakeFolderPollingInterval as seconds (so convert to ms)
|
||||
const interval = (this.db.data.config.intakeFolderPollingInterval || DefaultConfig.intakeFolderPollingInterval) * 1000;
|
||||
|
||||
if (interval < 30000) {
|
||||
this.plugin.logger.warn('DataManager: Intake folder polling interval is less than 30 seconds. This is not recommended.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.pollIntervalId = window.setInterval(
|
||||
() => this.plugin.screenshotProcessor.processIntakeFolderAuto(),
|
||||
interval
|
||||
);
|
||||
}
|
||||
|
||||
stopIntakeDirectoryPolling() {
|
||||
if (this.pollIntervalId) {
|
||||
window.clearInterval(this.pollIntervalId);
|
||||
this.pollIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check for unprocessed images and trigger processing */
|
||||
/** Keeping for posterity sake as the application needs change */
|
||||
private async checkForUnprocessedImages() {
|
||||
const periodicProcessingEnabled = await this.plugin.periodicProcessingEnabled();
|
||||
if (!periodicProcessingEnabled) return;
|
||||
|
|
@ -678,24 +634,24 @@ export class DataManager extends Events {
|
|||
|
||||
// Check if screenshot file exists
|
||||
if (entry.screenshotStoragePath) {
|
||||
const screenshotExists = await this.plugin.app.vault.adapter.exists(entry.screenshotStoragePath);
|
||||
if (!screenshotExists) {
|
||||
const screenshotExists = this.plugin.app.vault.getAbstractFileByPath(entry.screenshotStoragePath);
|
||||
if (screenshotExists instanceof TFile) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if note file exists
|
||||
if (entry.notePath) {
|
||||
const noteExists = await this.plugin.app.vault.adapter.exists(entry.notePath);
|
||||
if (!noteExists) {
|
||||
const noteExists = this.plugin.app.vault.getAbstractFileByPath(entry.notePath);
|
||||
if (noteExists instanceof TFile) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if metadata file exists
|
||||
if (entry.metadataPath) {
|
||||
const metadataExists = await this.plugin.app.vault.adapter.exists(entry.metadataPath);
|
||||
if (!metadataExists) {
|
||||
const metadataExists = this.plugin.app.vault.getAbstractFileByPath(entry.metadataPath);
|
||||
if (metadataExists instanceof TFile) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class StatusBarManager {
|
|||
}
|
||||
|
||||
this.setStatusBarIcon(queueIndicator, icon);
|
||||
setTooltip(this.statusBarProcessingQueueEl, 'Processing Queue', { placement: 'top' });
|
||||
setTooltip(this.statusBarProcessingQueueEl, 'Processing queue', { placement: 'top' });
|
||||
queueIndicator.onclick = () => this.plugin.openProcessingQueueModal();
|
||||
|
||||
if (isProcessing && !isPaused) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TFile, normalizePath, FileSystemAdapter } from 'obsidian';
|
||||
import { TFile, normalizePath, Vault } from 'obsidian';
|
||||
import { base64ToExtension } from './encode';
|
||||
import VisionRecallPlugin from '@/main';
|
||||
|
||||
|
|
@ -18,21 +18,21 @@ export async function saveBase64ImageInVault(plugin: VisionRecallPlugin, base64:
|
|||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// Get the Obsidian filesystem
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
if (!(adapter instanceof FileSystemAdapter)) {
|
||||
const vault = plugin.app.vault;
|
||||
if (!(vault instanceof Vault)) {
|
||||
plugin.logger.error("This Obsidian instance does not support file writing.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the folder exists
|
||||
const folderExists = await adapter.exists(folderPath);
|
||||
const folderExists = vault.getFolderByPath(folderPath);
|
||||
if (!folderExists) {
|
||||
// await adapter.mkdir(folderPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Write the file
|
||||
await adapter.writeBinary(filePath, buffer);
|
||||
await vault.createBinary(filePath, buffer);
|
||||
|
||||
// Return the saved file
|
||||
const file = plugin.app.vault.getAbstractFileByPath(filePath);
|
||||
|
|
|
|||
85
src/main.ts
85
src/main.ts
|
|
@ -4,7 +4,7 @@ import { PLUGIN_ICON, PLUGIN_NAME } from '@/constants';
|
|||
import ScreenshotKBSettingTab from '@/settings/SettingsPage';
|
||||
import { VisionRecallPluginSettings, DEFAULT_SETTINGS } from '@/types/settings-types';
|
||||
import { ScreenshotProcessor } from '@/services/screenshot-processor';
|
||||
import "@/styles/global.css";
|
||||
import "@/styles/global.scss";
|
||||
import { DataManager, StoredData } from './data/DataManager';
|
||||
import { PluginLogger } from '@/lib/Logger';
|
||||
import { shouldProcessImage } from '@/lib/image-utils';
|
||||
|
|
@ -15,6 +15,7 @@ import { useQueueStore } from '@/stores/queueStore';
|
|||
import { StatusBarManager } from './lib/StatusBarManager';
|
||||
import { debounce } from 'obsidian';
|
||||
import { customParse } from './lib/json-utils';
|
||||
import { DefaultConfig } from './types/config-types';
|
||||
|
||||
export default class VisionRecallPlugin extends Plugin {
|
||||
settings: VisionRecallPluginSettings;
|
||||
|
|
@ -37,12 +38,11 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
this.processingQueue = new ProcessingQueue(this, useQueueStore);
|
||||
this.statusBarManager = new StatusBarManager(this);
|
||||
|
||||
this.initializeProcessing();
|
||||
this.initializeUI();
|
||||
this.registerEventListeners();
|
||||
await this.registerEventListeners();
|
||||
this.registerCommands();
|
||||
this.registerProtocolHandler();
|
||||
await this.startBackgroundProcesses();
|
||||
await this.loadScreenshotMetadata();
|
||||
this.logger.info('Plugin loaded successfully');
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -51,15 +51,10 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
// ... (rest of the methods, as before, BUT WITH CHANGES BELOW) ...
|
||||
private async initializeDataManagement() {
|
||||
await this.dataManager.init();
|
||||
}
|
||||
|
||||
private initializeProcessing() {
|
||||
// Nothing specific to do here now.
|
||||
}
|
||||
|
||||
private initializeUI() {
|
||||
this.statusBarManager.initialize();
|
||||
|
||||
|
|
@ -75,8 +70,7 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
private registerEventListeners() {
|
||||
private async registerEventListeners() {
|
||||
// Debounce the file creation handler
|
||||
this.debouncedOnCreateHandler = debounce(this.onFileCreated.bind(this), 500, true);
|
||||
|
||||
|
|
@ -84,6 +78,25 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
this.app.vault.on('create', this.debouncedOnCreateHandler)
|
||||
);
|
||||
|
||||
if (await this.periodicProcessingEnabled()) {
|
||||
const config = this.dataManager.getConfig();
|
||||
// consider intakeFolderPollingInterval as seconds (so convert to ms)
|
||||
const interval = (config.intakeFolderPollingInterval || DefaultConfig.intakeFolderPollingInterval) * 1000;
|
||||
|
||||
|
||||
if (interval < 30000) {
|
||||
this.logger.warn('DataManager: Intake folder polling interval is less than 30 seconds. This is not recommended.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerEvent(
|
||||
window.setInterval(
|
||||
() => this.screenshotProcessor.processIntakeFolderAuto(),
|
||||
interval
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
this.dataManager.on('unprocessed-images-found', async () => {
|
||||
await this.screenshotProcessor.processIntakeFolderAuto();
|
||||
});
|
||||
|
|
@ -93,7 +106,7 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
this.addCommand({
|
||||
id: 'open-main-view',
|
||||
name: 'Open main view',
|
||||
callback: async () => this.activateView()
|
||||
callback: async () => this.openView()
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
|
|
@ -164,31 +177,18 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
private async startBackgroundProcesses() {
|
||||
await this.loadScreenshotMetadata();
|
||||
|
||||
this.registerInterval(
|
||||
window.setInterval(() => this.logger.debug('Interval check'), 5 * 60 * 1000)
|
||||
);
|
||||
|
||||
if (await this.periodicProcessingEnabled()) {
|
||||
await this.dataManager.startIntakeDirectoryPolling();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
openProcessingQueueModal() {
|
||||
const modal = new ProcessingQueueModal({ app: this.app, plugin: this });
|
||||
modal.open();
|
||||
}
|
||||
|
||||
async autoProcessingEnabled(): Promise<boolean> {
|
||||
const config = await this.dataManager.getConfig();
|
||||
const config = this.dataManager.getConfig();
|
||||
return config.enableAutoIntakeFolderProcessing;
|
||||
}
|
||||
|
||||
async periodicProcessingEnabled(): Promise<boolean> {
|
||||
const config = await this.dataManager.getConfig();
|
||||
const config = this.dataManager.getConfig();
|
||||
return config.enablePeriodicIntakeFolderProcessing;
|
||||
}
|
||||
|
||||
|
|
@ -209,7 +209,6 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
onunload() {
|
||||
if (this.screenshotProcessor) {
|
||||
this.screenshotProcessor.terminateWorker();
|
||||
|
|
@ -233,33 +232,6 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
async activateView() {
|
||||
this.app.workspace.detachLeavesOfType(MAIN_VIEW_TYPE);
|
||||
await this.app.workspace.getLeaf(true).setViewState({
|
||||
type: MAIN_VIEW_TYPE,
|
||||
active: true,
|
||||
});
|
||||
|
||||
this.app.workspace.revealLeaf(
|
||||
this.app.workspace.getLeavesOfType(MAIN_VIEW_TYPE)[0]
|
||||
);
|
||||
}
|
||||
|
||||
toggleView() {
|
||||
const leaves = this.app.workspace.getLeavesOfType(MAIN_VIEW_TYPE);
|
||||
if (leaves.length > 0) {
|
||||
this.deactivateView();
|
||||
} else {
|
||||
this.openView();
|
||||
}
|
||||
}
|
||||
|
||||
async deactivateView() {
|
||||
this.app.workspace.detachLeavesOfType(MAIN_VIEW_TYPE);
|
||||
}
|
||||
|
||||
|
||||
async loadSettings() {
|
||||
const loadedData = await this.loadData();
|
||||
const parsedData = customParse(loadedData) as Partial<StoredData>;
|
||||
|
|
@ -290,7 +262,6 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
await this.saveData(mergedData);
|
||||
}
|
||||
|
||||
|
||||
async loadScreenshotMetadata(): Promise<void> {
|
||||
this.metadata = this.dataManager.getAllEntries();
|
||||
}
|
||||
|
|
@ -317,7 +288,7 @@ export default class VisionRecallPlugin extends Plugin {
|
|||
for (const child of children) {
|
||||
if (child instanceof TFile && child.extension === 'json') {
|
||||
try {
|
||||
const metadataContent = await this.app.vault.read(child);
|
||||
const metadataContent = await this.app.vault.cachedRead(child);
|
||||
const metadata = JSON.parse(metadataContent);
|
||||
metadataArray.push(metadata);
|
||||
metadata?.extractedTags?.forEach(tag => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { OPENROUTER_HEADERS } from '@/constants';
|
||||
import { tagsJsonSchema, TagsSchema } from '@/lib/tag-utils';
|
||||
import { VisionRecallPluginSettings } from '@/types/settings-types';
|
||||
import { requestUrl, RequestUrlParam, RequestUrlResponse } from 'obsidian';
|
||||
|
||||
export const VISION_LLM_PROMPT = "Analyze this screenshot and describe its content and identify the type of screenshot if possible.";
|
||||
|
||||
|
|
@ -40,18 +41,22 @@ export async function callLLMAPI(
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${apiEndpoint}`, {
|
||||
const request: RequestUrlParam = {
|
||||
url: `${baseUrl}${apiEndpoint}`,
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`LLM API Error: ${response.status} ${response.statusText}`);
|
||||
const response: RequestUrlResponse = await requestUrl(request);
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error(`LLM API Error: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// const data = await response.json();
|
||||
const data = response.json;
|
||||
|
||||
if (settings.llmProvider === 'openai' || settings.llmProvider === 'ollama') {
|
||||
return data?.choices?.[0]?.message?.content || null;
|
||||
|
|
@ -150,8 +155,6 @@ export async function llmSuggestTagsAndTitle(settings: VisionRecallPluginSetting
|
|||
return DEFAULT_TAGS_AND_TITLE;
|
||||
}
|
||||
|
||||
console.log('LLM Response Text:', llmResponseText);
|
||||
|
||||
try {
|
||||
const { title, tags } = extractTitleAndTags(llmResponseText);
|
||||
return {
|
||||
|
|
@ -218,17 +221,20 @@ export async function fetchLLMAPIGet(
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${apiEndpoint}`, {
|
||||
const request: RequestUrlParam = {
|
||||
url: `${baseUrl}${apiEndpoint}`,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`LLM API Error: ${response.status} ${response.statusText}`);
|
||||
const response = await requestUrl(request);
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error(`LLM API Error: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = response.json;
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
|
|
@ -267,13 +273,13 @@ type OllamaApiResponse = {
|
|||
async function fetchOllamaModels(endpointUrl: string): Promise<OllamaModel[]> {
|
||||
try {
|
||||
let endpoint = adjustEndpoint(endpointUrl, true)
|
||||
const response = await fetch(`${endpoint}/api/tags`);
|
||||
const response = await requestUrl(`${endpoint}/api/tags`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch models: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: OllamaApiResponse = await response.json();
|
||||
const data: OllamaApiResponse = response.json;
|
||||
return data.models;
|
||||
} catch (error) {
|
||||
console.error("Error fetching Ollama models:", error);
|
||||
|
|
@ -301,18 +307,21 @@ type OpenAIModelsResponse = {
|
|||
async function fetchOpenAIModels(endpointUrl: string, apiKey: string): Promise<OpenAIModel[]> {
|
||||
try {
|
||||
let endpoint = adjustEndpoint(endpointUrl, true)
|
||||
const response = await fetch(`${endpoint}/v1/models`, {
|
||||
const request: RequestUrlParam = {
|
||||
url: `${endpoint}/v1/models`,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
||||
const response = await requestUrl(request);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch models: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: OpenAIModelsResponse = await response.json();
|
||||
const data: OpenAIModelsResponse = response.json;
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching OpenAI models:", error);
|
||||
|
|
|
|||
|
|
@ -1,45 +1,11 @@
|
|||
import VisionRecallPlugin from '@/main';
|
||||
import { Notice, Plugin, TFile } from 'obsidian';
|
||||
|
||||
// Generated by ChatGPT and not part of the rest of the functions below
|
||||
export const findNotesWithTag = (plugin: VisionRecallPlugin, tag: string): string[] => {
|
||||
const filesWithTag: string[] = [];
|
||||
const cachedFiles = Object.keys(plugin.app.metadataCache.getCache('files') || {});
|
||||
cachedFiles.forEach((filePath) => {
|
||||
const metadata = plugin.app.metadataCache.getCache(filePath);
|
||||
|
||||
if (metadata && metadata.tags) {
|
||||
const tags = metadata.tags.map(tagObj => tagObj.tag);
|
||||
if (tags.includes(tag)) {
|
||||
filesWithTag.push(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return filesWithTag;
|
||||
}
|
||||
|
||||
export const findFirstNoteWithTag = async (plugin: Plugin, tag: string): Promise<string | null> => {
|
||||
const cachedFiles = Object.keys(plugin.app.metadataCache.getCache('files') || {});
|
||||
cachedFiles.forEach((filePath) => {
|
||||
const metadata = plugin.app.metadataCache.getCache(filePath);
|
||||
|
||||
if (metadata && metadata.tags) {
|
||||
const tags = metadata.tags.map(tagObj => tagObj.tag);
|
||||
if (tags.includes(tag)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const findNotesWithTagInline = async (plugin: VisionRecallPlugin, tag: string): Promise<string[]> => {
|
||||
const matchingFiles: string[] = [];
|
||||
|
||||
for (const file of plugin.app.vault.getMarkdownFiles()) {
|
||||
const content = await plugin.app.vault.read(file);
|
||||
const content = await plugin.app.vault.cachedRead(file);
|
||||
if (content.includes(tag)) {
|
||||
matchingFiles.push(file.path);
|
||||
}
|
||||
|
|
@ -48,27 +14,6 @@ export const findNotesWithTagInline = async (plugin: VisionRecallPlugin, tag: st
|
|||
return matchingFiles;
|
||||
}
|
||||
|
||||
export const openNoteWithTagOld = async (plugin: VisionRecallPlugin, tag: string): Promise<void> => {
|
||||
const file = await findFirstNoteWithTag(plugin, tag);
|
||||
if (file) {
|
||||
await plugin.app.workspace.openLinkText(file, '', false, { active: true });
|
||||
} else {
|
||||
plugin.logger.info(`No notes found with tag ${tag}`);
|
||||
}
|
||||
|
||||
// const files = findNotesWithTag(plugin, tag);
|
||||
// if (files.length > 0) {
|
||||
// const file = plugin.app.vault.getAbstractFileByPath(files[0]);
|
||||
// if (file instanceof TFile) {
|
||||
// // plugin.app.workspace.getMostRecentLeaf().openFile(file); // might want to open in a new tab instead
|
||||
// await plugin.app.workspace.openLinkText(file.path, '', false, { active: true });
|
||||
// }
|
||||
// } else {
|
||||
// console.log(`No notes found with tag ${tag}`);
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions for Note Linking by Full Unique Tag (Using metadataCache) ---
|
||||
|
||||
export async function getNoteByUniqueTagOrCreateTag(plugin: VisionRecallPlugin, noteName: string, parentTagPrefix: string): Promise<TFile | null> {
|
||||
|
|
@ -119,7 +64,7 @@ export async function findNoteByUniqueTag(plugin: VisionRecallPlugin, fullUnique
|
|||
|
||||
export async function ensureUniqueTagExists(plugin: VisionRecallPlugin, file: TFile, parentTagPrefix: string): Promise<string | null> {
|
||||
const { vault } = plugin.app;
|
||||
let fileContent = await vault.read(file);
|
||||
let fileContent = await vault.cachedRead(file);
|
||||
const existingTag = await findUniqueTagInContent(fileContent, parentTagPrefix);
|
||||
|
||||
if (existingTag) {
|
||||
|
|
@ -131,7 +76,11 @@ export async function ensureUniqueTagExists(plugin: VisionRecallPlugin, file: TF
|
|||
|
||||
// Append the new tag to the end of the file content
|
||||
const updatedContent = fileContent.trimEnd() + `\n\n${newUniqueTag}`;
|
||||
await vault.modify(file, updatedContent);
|
||||
|
||||
vault.process(file, (data: string) => {
|
||||
return updatedContent;
|
||||
});
|
||||
|
||||
return newUniqueTag;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { App, Notice, TFile, TFolder, normalizePath } from 'obsidian';
|
||||
import { App, FileManager, Notice, TFile, TFolder, normalizePath } from 'obsidian';
|
||||
import Tesseract, { createWorker, Worker } from 'tesseract.js';
|
||||
import { VisionRecallPluginSettings } from '@/types/settings-types';
|
||||
import { DEFAULT_TAGS_AND_TITLE, TagsAndTitle, VISION_LLM_PROMPT, callLLMAPI, llmSuggestTagsAndTitle } from '@/services/llm-service';
|
||||
|
|
@ -196,8 +196,9 @@ export class ScreenshotProcessor {
|
|||
newScreenshotPath: string;
|
||||
}> {
|
||||
const screenshotStorageFolder = normalizePath(await this.plugin.getFolderFromSettingsKey('screenshotStorageFolderPath'));
|
||||
if (!await this.app.vault.adapter.exists(screenshotStorageFolder)) {
|
||||
await this.app.vault.adapter.mkdir(screenshotStorageFolder);
|
||||
const screenshotStorageFolderExists = this.app.vault.getFolderByPath(screenshotStorageFolder);
|
||||
if (!screenshotStorageFolderExists) {
|
||||
await this.app.vault.createFolder(screenshotStorageFolder);
|
||||
}
|
||||
|
||||
// const timestamp = Date.now();
|
||||
|
|
@ -611,17 +612,26 @@ export class ScreenshotProcessor {
|
|||
|
||||
this.plugin.logger.debug(`Deleting metadata for ${params.identity}`);
|
||||
if (currentMetadata?.metadataPath) {
|
||||
await this.app.vault.adapter.trashLocal(currentMetadata.metadataPath);
|
||||
const metadataFile = this.app.vault.getAbstractFileByPath(currentMetadata.metadataPath);
|
||||
if (metadataFile instanceof TFile) {
|
||||
await this.app.fileManager.trashFile(metadataFile);
|
||||
}
|
||||
} else {
|
||||
const screenshotFilename = currentMetadata.screenshotFilename.split('.').slice(0, -1).join('.');
|
||||
const metadataFilename = `${screenshotFilename}.json`;
|
||||
|
||||
const metadataPath = normalizePath(`${screenshotStorageFolder}/${metadataFilename}`);
|
||||
await this.app.vault.adapter.trashLocal(metadataPath);
|
||||
const metadataFile = this.app.vault.getAbstractFileByPath(metadataPath);
|
||||
if (metadataFile instanceof TFile) {
|
||||
await this.app.fileManager.trashFile(metadataFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMetadata?.screenshotStoragePath) {
|
||||
await this.app.vault.adapter.trashLocal(currentMetadata.screenshotStoragePath);
|
||||
const screenshotFile = this.app.vault.getAbstractFileByPath(currentMetadata.screenshotStoragePath);
|
||||
if (screenshotFile instanceof TFile) {
|
||||
await this.app.fileManager.trashFile(screenshotFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -654,7 +664,8 @@ export class ScreenshotProcessor {
|
|||
if (updatedMetadata.notePath) {
|
||||
const noteFile = this.app.vault.getAbstractFileByPath(updatedMetadata.notePath);
|
||||
if (noteFile instanceof TFile) {
|
||||
const currentNoteContent = await this.app.vault.read(noteFile);
|
||||
this.app.vault.process(noteFile, (data: string) => {
|
||||
const currentNoteContent = data;
|
||||
|
||||
// Replace the tags section in the note
|
||||
const formattedTags = tagsToCommaString(formatTags(updatedMetadata.extractedTags));
|
||||
|
|
@ -672,12 +683,15 @@ export class ScreenshotProcessor {
|
|||
);
|
||||
const finalNoteContent = `${updatedTitleAndNotes}---${restOfContent}`;
|
||||
|
||||
await this.app.vault.modify(noteFile, finalNoteContent);
|
||||
return finalNoteContent;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated metadata to the JSON file
|
||||
await this.app.vault.modify(file, JSON.stringify(updatedMetadata, null, 2));
|
||||
this.app.vault.process(file, (data: string) => {
|
||||
return JSON.stringify(updatedMetadata, null, 2);
|
||||
});
|
||||
|
||||
new Notice('Screenshot metadata updated successfully');
|
||||
} catch (error) {
|
||||
|
|
@ -720,8 +734,9 @@ export class ScreenshotProcessor {
|
|||
const extension = type.split('/')[1];
|
||||
const fileName = `clipboard-image-${timestamp}.${extension}`;
|
||||
|
||||
if (!await this.app.vault.adapter.exists(folderPath)) {
|
||||
await this.app.vault.adapter.mkdir(folderPath);
|
||||
const screenshotStorageFolder = this.app.vault.getFolderByPath(folderPath);
|
||||
if (!screenshotStorageFolder) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
|
||||
const filePath = normalizePath(`${folderPath}/${fileName}`);
|
||||
|
|
@ -786,7 +801,10 @@ export class ScreenshotProcessor {
|
|||
}
|
||||
|
||||
async deleteFile(filePath: string) {
|
||||
await this.app.vault.adapter.trashLocal(filePath);
|
||||
const fileToDelete = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (fileToDelete instanceof TFile) {
|
||||
await this.app.fileManager.trashFile(fileToDelete);
|
||||
}
|
||||
}
|
||||
|
||||
async processIntakeFolderAuto() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import VisionRecallPlugin from '@/main';
|
||||
import { normalizePath } from 'obsidian';
|
||||
|
||||
export async function initializeFolders(plugin: VisionRecallPlugin) {
|
||||
try {
|
||||
|
|
@ -30,10 +31,16 @@ export async function initializeFolders(plugin: VisionRecallPlugin) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a filename by replacing problematic characters with an empty string
|
||||
* This is used in addition to normalizePath() (which is used 20+ times in this project)
|
||||
* @param input - The filename to sanitize
|
||||
* @returns The sanitized filename
|
||||
*/
|
||||
export function sanitizeFilename(input: string): string {
|
||||
// Define a regex pattern to match problematic filename characters
|
||||
const forbiddenChars = /[<>:"\/\\|?*\x00-\x1F]/g
|
||||
|
||||
// Replace them with an empty string
|
||||
return input.replace(forbiddenChars, '')
|
||||
return normalizePath(input.replace(forbiddenChars, ''))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ export default class VisionRecallSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value: 'openai' | 'ollama') => {
|
||||
this.plugin.settings.llmProvider = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.llmProvider != 'ollama') {
|
||||
new Setting(containerEl)
|
||||
.setName('API key')
|
||||
.setDesc('OpenAI API key (not needed for Ollama).')
|
||||
|
|
@ -42,6 +44,7 @@ export default class VisionRecallSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('API base URL')
|
||||
|
|
@ -89,9 +92,11 @@ export default class VisionRecallSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value) => {
|
||||
this.plugin.settings.useParentFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.useParentFolder) {
|
||||
new Setting(containerEl)
|
||||
.setName('Parent folder path')
|
||||
.setDesc('Path to the parent folder. Optional if not using parent folder organization.')
|
||||
|
|
@ -103,6 +108,7 @@ export default class VisionRecallSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Add prefix to folder names')
|
||||
|
|
@ -112,9 +118,11 @@ export default class VisionRecallSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value) => {
|
||||
this.plugin.settings.addPrefixToFolderNames = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.addPrefixToFolderNames) {
|
||||
new Setting(containerEl)
|
||||
.setName('Prefix to add to folder names')
|
||||
.setDesc('Prefix for folder names. Optional.')
|
||||
|
|
@ -126,6 +134,7 @@ export default class VisionRecallSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Screenshot storage folder')
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
.config-modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
/* padding: 0.5rem 0; */
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.config-field label {
|
||||
font-weight: 500;
|
||||
/* margin-bottom: 0.25rem; */
|
||||
}
|
||||
|
||||
.config-field-checkbox-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-field input[type='text'],
|
||||
.config-field input[type='number'],
|
||||
.config-field select {
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.config-multiselect {
|
||||
height: 100px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.config-modal-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
/* padding: 1rem 0; */
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Debug Operations Modal Styles */
|
||||
.debug-operations-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.debug-section h3 {
|
||||
margin: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.debug-description {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.debug-results {
|
||||
padding: 1rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.debug-results h4 {
|
||||
margin: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.results-details {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result-item span:first-child {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
115
src/styles/components/config-modal.scss
Normal file
115
src/styles/components/config-modal.scss
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
.vr {
|
||||
.config-modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
/* padding: 0.5rem 0; */
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.config-field label {
|
||||
font-weight: 500;
|
||||
/* margin-bottom: 0.25rem; */
|
||||
}
|
||||
|
||||
.config-field-checkbox-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-field input[type='text'],
|
||||
.config-field input[type='number'],
|
||||
.config-field select {
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.config-multiselect {
|
||||
height: 100px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.config-modal-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
/* padding: 1rem 0; */
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Debug Operations Modal Styles */
|
||||
.debug-operations-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.debug-section h3 {
|
||||
margin: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.debug-description {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.debug-results {
|
||||
padding: 1rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.debug-results h4 {
|
||||
margin: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.results-details {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result-item span:first-child {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
43
src/styles/components/delete-confirmation-modal.scss
Normal file
43
src/styles/components/delete-confirmation-modal.scss
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
.vr-delete-confirmation-modal {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal input,
|
||||
.vr-delete-confirmation-modal textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal .button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vr-delete-confirmation-modal button[type='submit'] {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
|
@ -1,43 +1,43 @@
|
|||
.edit-metadata-modal {
|
||||
.vr-edit-metadata-modal {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.edit-metadata-modal h3 {
|
||||
.vr-edit-metadata-modal h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.edit-metadata-modal .form-group {
|
||||
.vr-edit-metadata-modal .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.edit-metadata-modal label {
|
||||
.vr-edit-metadata-modal label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edit-metadata-modal input,
|
||||
.edit-metadata-modal textarea {
|
||||
.vr-edit-metadata-modal input,
|
||||
.vr-edit-metadata-modal textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.edit-metadata-modal .button-group {
|
||||
.vr-edit-metadata-modal .button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.edit-metadata-modal button {
|
||||
.vr-edit-metadata-modal button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-metadata-modal button[type='submit'] {
|
||||
.vr-edit-metadata-modal button[type='submit'] {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
.processing-queue-modal {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.queue-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.queue-control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.queue-control-button:hover {
|
||||
background-color: var(--background-modifier-active);
|
||||
}
|
||||
|
||||
.queue-status {
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.queue-items {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: var(--text-success);
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--text-muted);
|
||||
border-top-color: var(--text-normal);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.item-error {
|
||||
color: var(--text-error);
|
||||
font-size: 0.9em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
93
src/styles/components/processing-queue.scss
Normal file
93
src/styles/components/processing-queue.scss
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
.vr {
|
||||
.processing-queue-modal {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.queue-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.queue-control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.queue-control-button:hover {
|
||||
background-color: var(--background-modifier-active);
|
||||
}
|
||||
|
||||
.queue-status {
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.queue-items {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: var(--text-success);
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--text-muted);
|
||||
border-top-color: var(--text-normal);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.item-error {
|
||||
color: var(--text-error);
|
||||
font-size: 0.9em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
.tag-combobox {
|
||||
position: relative; /* For positioning suggestions */
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 5px;
|
||||
display: flex; /* For horizontal layout */
|
||||
flex-wrap: wrap; /* Allow tags to wrap */
|
||||
align-items: center; /* Vertically align items */
|
||||
min-height: 30px; /* Ensure a reasonable minimum height */
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
flex-grow: 1; /* Take up remaining space */
|
||||
padding: 5px;
|
||||
min-width: 100px; /* Prevent collapsing when empty */
|
||||
}
|
||||
|
||||
.selected-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: 5px; /* Spacing from input */
|
||||
}
|
||||
|
||||
.tag-pill {
|
||||
/* background-color: var(--interactive-accent); */
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--text-on-accent);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
margin: 2px;
|
||||
display: inline-flex; /* For button alignment */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remove-tag-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
font-size: 0.8em;
|
||||
padding: 0; /* Reset padding */
|
||||
line-height: 1; /* Reset line-height */
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.suggestions-list {
|
||||
position: absolute; /* Position below the input */
|
||||
top: 100%; /* Below the tag-combobox */
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
/* border-top: none; */
|
||||
/* border-radius: 0 0 4px 4px; */
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 10; /* Ensure it's above other content */
|
||||
max-height: 200px; /* Limit height for scrollability */
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.suggestions-list li {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggestions-list li:hover {
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
81
src/styles/components/tag-combobox.scss
Normal file
81
src/styles/components/tag-combobox.scss
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
.vr {
|
||||
.tag-combobox {
|
||||
position: relative; /* For positioning suggestions */
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 5px;
|
||||
display: flex; /* For horizontal layout */
|
||||
flex-wrap: wrap; /* Allow tags to wrap */
|
||||
align-items: center; /* Vertically align items */
|
||||
min-height: 30px; /* Ensure a reasonable minimum height */
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
flex-grow: 1; /* Take up remaining space */
|
||||
padding: 5px;
|
||||
min-width: 100px; /* Prevent collapsing when empty */
|
||||
}
|
||||
|
||||
.selected-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: 5px; /* Spacing from input */
|
||||
}
|
||||
|
||||
.tag-pill {
|
||||
/* background-color: var(--interactive-accent); */
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--text-on-accent);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
margin: 2px;
|
||||
display: inline-flex; /* For button alignment */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remove-tag-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
font-size: 0.8em;
|
||||
padding: 0; /* Reset padding */
|
||||
line-height: 1; /* Reset line-height */
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.suggestions-list {
|
||||
position: absolute; /* Position below the input */
|
||||
top: 100%; /* Below the tag-combobox */
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
/* border-top: none; */
|
||||
/* border-radius: 0 0 4px 4px; */
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 10; /* Ensure it's above other content */
|
||||
max-height: 200px; /* Limit height for scrollability */
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.suggestions-list li {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggestions-list li:hover {
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
.view-metadata-modal {
|
||||
/* padding: 20px; */
|
||||
/* max-width: 600px; */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-metadata-modal h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.view-metadata-modal h4 {
|
||||
/* margin: 15px 0 10px; */
|
||||
margin: 0 0 0 0;
|
||||
padding-bottom: 10px;
|
||||
font-size: 1.1em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.metadata-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.metadata-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.metadata-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.metadata-value {
|
||||
padding: 8px;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.metadata-value.scrollable {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.metadata-value.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.no-tags {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: var(--interactive-normal);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.button-group button:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
96
src/styles/components/view-metadata-modal.scss
Normal file
96
src/styles/components/view-metadata-modal.scss
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
.vr {
|
||||
.view-metadata-modal {
|
||||
/* padding: 20px; */
|
||||
/* max-width: 600px; */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-metadata-modal h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.view-metadata-modal h4 {
|
||||
/* margin: 15px 0 10px; */
|
||||
margin: 0 0 0 0;
|
||||
padding-bottom: 10px;
|
||||
font-size: 1.1em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.metadata-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.metadata-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.metadata-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.metadata-value {
|
||||
padding: 8px;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.metadata-value.scrollable {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.metadata-value.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.no-tags {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: var(--interactive-normal);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.button-group button:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
@import './components/processing-display.css';
|
||||
@import './components/edit-metadata-modal.css';
|
||||
@import './components/loaders.css';
|
||||
@import './components/view-metadata-modal.css';
|
||||
@import './components/config-modal.css';
|
||||
@import './components/processing-queue.css';
|
||||
@import './components/tag-combobox.css';
|
||||
@use './components/processing-display.scss';
|
||||
@use './components/edit-metadata-modal.scss';
|
||||
@use './components/loaders.scss';
|
||||
@use './components/view-metadata-modal.scss';
|
||||
@use './components/config-modal.scss';
|
||||
@use './components/processing-queue.scss';
|
||||
@use './components/tag-combobox.scss';
|
||||
@use './components/delete-confirmation-modal.scss';
|
||||
@tailwind base;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
.vision-recall-styling {
|
||||
--dialog-width: calc(min(auto, 100%));
|
||||
--dialog-height: calc(min(auto, 100%));
|
||||
}
|
||||
|
||||
.vision-recall-styling {
|
||||
.settings-display {
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
|
|
@ -38,6 +40,7 @@
|
|||
.setting-value {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 1rem;
|
||||
|
|
@ -96,7 +99,7 @@
|
|||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.loader {
|
||||
.vr-loader {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: #000;
|
||||
border-radius: 50%;
|
||||
|
|
@ -125,9 +128,9 @@
|
|||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
/* .settings-header {
|
||||
margin-top: 40px;
|
||||
}
|
||||
} */
|
||||
|
||||
.vision-recall-view {
|
||||
padding-right: 20px;
|
||||
|
|
@ -7,6 +7,7 @@ import { useDataContext } from '@/data/DataContext';
|
|||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { STORAGE_KEYS, DEFAULT_ITEMS_PER_PAGE } from '@/constants';
|
||||
import TagCombobox from '@/components/ui/tag-combobox';
|
||||
import { App } from 'obsidian';
|
||||
|
||||
export interface BaseViewProps {
|
||||
initialMetadata: any[];
|
||||
|
|
@ -54,7 +55,7 @@ export const useBaseView = ({ initialMetadata, viewMode, onViewModeChange }: Bas
|
|||
|
||||
const [filteredMetadata, setFilteredMetadata] = useState([]);
|
||||
const [foldersInitialized, setFoldersInitialized] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem('vision-recall-initialize-folders');
|
||||
const saved = app.loadLocalStorage('vision-recall-initialize-folders');
|
||||
return saved ? saved === 'true' : false;
|
||||
});
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ export const useBaseView = ({ initialMetadata, viewMode, onViewModeChange }: Bas
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEYS.ITEMS_PER_PAGE);
|
||||
const saved = app.loadLocalStorage(STORAGE_KEYS.ITEMS_PER_PAGE);
|
||||
const parsed = saved ? Number(saved) : DEFAULT_ITEMS_PER_PAGE;
|
||||
return isNaN(parsed) ? DEFAULT_ITEMS_PER_PAGE : parsed;
|
||||
} catch (e) {
|
||||
|
|
@ -92,7 +93,7 @@ export const useBaseView = ({ initialMetadata, viewMode, onViewModeChange }: Bas
|
|||
|
||||
const [sortCriteria, setSortCriteria] = useState<SortCriteria>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEYS.SORT_CRITERIA);
|
||||
const saved = app.loadLocalStorage(STORAGE_KEYS.SORT_CRITERIA);
|
||||
const parsed = saved ? JSON.parse(saved) : {
|
||||
column: 'timestamp',
|
||||
direction: 'desc'
|
||||
|
|
@ -115,7 +116,7 @@ export const useBaseView = ({ initialMetadata, viewMode, onViewModeChange }: Bas
|
|||
direction: sortCriteria.column === columnName && sortCriteria.direction === 'asc' ? 'desc' : 'asc', // Toggle direction if same column is clicked
|
||||
} as SortCriteria;
|
||||
setSortCriteria(newSortCriteria);
|
||||
localStorage.setItem(STORAGE_KEYS.SORT_CRITERIA, JSON.stringify(newSortCriteria));
|
||||
app.saveLocalStorage(STORAGE_KEYS.SORT_CRITERIA, JSON.stringify(newSortCriteria));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save sort criteria:', e);
|
||||
}
|
||||
|
|
@ -256,7 +257,7 @@ export const useBaseView = ({ initialMetadata, viewMode, onViewModeChange }: Bas
|
|||
try {
|
||||
setItemsPerPage(newValue);
|
||||
setCurrentPage(1);
|
||||
localStorage.setItem(STORAGE_KEYS.ITEMS_PER_PAGE, newValue.toString());
|
||||
app.saveLocalStorage(STORAGE_KEYS.ITEMS_PER_PAGE, newValue.toString());
|
||||
} catch (e) {
|
||||
console.warn('Failed to save items per page preference:', e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { MainViewHeader } from '@/components/MainViewHeader';
|
|||
import { ViewMetadataModal } from '../components/modals/ViewMetadataModal';
|
||||
import { openNoteWithTag } from '@/services/note-link-service';
|
||||
import { BaseViewProps, useBaseView } from './BaseView';
|
||||
import { DeleteConfirmationModal } from '@/components/modals/DeleteConfirmationModal';
|
||||
|
||||
const GalleryView = (props: BaseViewProps) => {
|
||||
const {
|
||||
|
|
@ -122,15 +123,20 @@ const GalleryView = (props: BaseViewProps) => {
|
|||
<span
|
||||
aria-label="Delete screenshot"
|
||||
className="w-5 h-5 cursor-pointer"
|
||||
onClick={async () => {
|
||||
const shouldDelete = window.confirm('Are you sure you want to delete this screenshot and its metadata?');
|
||||
if (shouldDelete) {
|
||||
onClick={() => {
|
||||
const modal = new DeleteConfirmationModal(app, plugin, 'Are you sure you want to delete this screenshot and its metadata?', async () => {
|
||||
await plugin.screenshotProcessor.deleteScreenshotMetadata({
|
||||
identity: item.id,
|
||||
identityType: 'id'
|
||||
});
|
||||
await refreshMetadata();
|
||||
// modal.close();
|
||||
},
|
||||
() => {
|
||||
modal.close();
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
|
|
@ -156,7 +162,7 @@ const GalleryView = (props: BaseViewProps) => {
|
|||
{DateTime.fromISO(item.timestamp).toFormat('yyyy/MM/dd HH:mm')}
|
||||
</div>
|
||||
<div className="gallery-tags">
|
||||
{item.extractedTags ? item.extractedTags.join(', ') : 'No Tags'}
|
||||
{item.extractedTags ? item.extractedTags.join(', ') : 'No tags'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { MainViewHeader } from '@/components/MainViewHeader';
|
|||
import { ViewMetadataModal } from '../components/modals/ViewMetadataModal';
|
||||
import { openNoteWithTag } from '@/services/note-link-service';
|
||||
import { BaseViewProps, useBaseView } from './BaseView';
|
||||
import { DeleteConfirmationModal } from '@/components/modals/DeleteConfirmationModal';
|
||||
|
||||
const ListView = (props: BaseViewProps) => {
|
||||
const {
|
||||
|
|
@ -102,7 +103,7 @@ const ListView = (props: BaseViewProps) => {
|
|||
{item.title}
|
||||
</span>
|
||||
<span>{DateTime.fromISO(item.timestamp).toFormat('yyyy/MM/dd HH:mm')}</span>
|
||||
<span>{item.extractedTags ? item.extractedTags.join(', ') : 'No Tags'}</span>
|
||||
<span>{item.extractedTags ? item.extractedTags.join(', ') : 'No tags'}</span>
|
||||
<span className='flex flex-row items-center gap-2'>
|
||||
<button
|
||||
aria-label='View metadata'
|
||||
|
|
@ -137,14 +138,18 @@ const ListView = (props: BaseViewProps) => {
|
|||
aria-label='Delete screenshot'
|
||||
className='cursor-pointer'
|
||||
onClick={async () => {
|
||||
const shouldDelete = window.confirm('Are you sure you want to delete this screenshot and its metadata?');
|
||||
if (shouldDelete) {
|
||||
const modal = new DeleteConfirmationModal(app, plugin, 'Are you sure you want to delete this screenshot and its metadata?', async () => {
|
||||
await plugin.screenshotProcessor.deleteScreenshotMetadata({
|
||||
identity: item.timestamp,
|
||||
identityType: 'timestamp'
|
||||
});
|
||||
await refreshMetadata();
|
||||
},
|
||||
() => {
|
||||
modal.close();
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}}>
|
||||
<Trash2 className='w-4 h-4' />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default class MainView extends ItemView {
|
|||
const metadata = this.plugin.dataManager.getAllEntries();
|
||||
|
||||
// Create wrapper div for React
|
||||
const reactContainer = container.createDiv();
|
||||
const reactContainer = container.createDiv({ cls: 'vision-recall-styling' });
|
||||
|
||||
// Create React root and render app
|
||||
this.root = createRoot(reactContainer);
|
||||
|
|
@ -76,7 +76,7 @@ const ViewContainer = ({ initialMetadata }) => {
|
|||
|
||||
const [viewMode, setViewMode] = React.useState<'list' | 'gallery'>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('vision-recall-view-mode');
|
||||
const saved = plugin.app.loadLocalStorage('vision-recall-view-mode');
|
||||
return saved === 'gallery' ? 'gallery' : 'list';
|
||||
} catch {
|
||||
// return 'list';
|
||||
|
|
@ -87,7 +87,7 @@ const ViewContainer = ({ initialMetadata }) => {
|
|||
const handleViewModeChange = (mode: 'list' | 'gallery') => {
|
||||
setViewMode(mode);
|
||||
try {
|
||||
localStorage.setItem('vision-recall-view-mode', mode);
|
||||
plugin.app.saveLocalStorage('vision-recall-view-mode', mode);
|
||||
} catch (e) {
|
||||
plugin.logger.error('Failed to save view mode preference:', e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"jsx": "react",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "types/*.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
"1.0.0": "0.15.0",
|
||||
"1.0.1": "0.15.0",
|
||||
"1.0.2": "0.15.0",
|
||||
"1.0.3": "0.15.0"
|
||||
"1.0.3": "0.15.0",
|
||||
"1.0.4": "1.8.3"
|
||||
}
|
||||
|
|
@ -11,7 +11,15 @@ export default defineConfig(async ({ mode }) => {
|
|||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@": path.resolve(__dirname, "./src")
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// You can add global Sass variables here if needed
|
||||
// additionalData: `@import "@/styles/variables.scss";`,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue