@meechi-ai/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +624 -0
- package/README.md +59 -0
- package/dist/components/CalendarView.d.ts +3 -0
- package/dist/components/CalendarView.js +72 -0
- package/dist/components/ChatInterface.d.ts +6 -0
- package/dist/components/ChatInterface.js +105 -0
- package/dist/components/FileExplorer.d.ts +9 -0
- package/dist/components/FileExplorer.js +757 -0
- package/dist/components/Icon.d.ts +9 -0
- package/dist/components/Icon.js +44 -0
- package/dist/components/SourceEditor.d.ts +13 -0
- package/dist/components/SourceEditor.js +50 -0
- package/dist/components/ThemeProvider.d.ts +5 -0
- package/dist/components/ThemeProvider.js +105 -0
- package/dist/components/ThemeSwitcher.d.ts +1 -0
- package/dist/components/ThemeSwitcher.js +16 -0
- package/dist/components/voice/VoiceInputArea.d.ts +14 -0
- package/dist/components/voice/VoiceInputArea.js +190 -0
- package/dist/components/voice/VoiceOverlay.d.ts +7 -0
- package/dist/components/voice/VoiceOverlay.js +71 -0
- package/dist/hooks/useMeechi.d.ts +16 -0
- package/dist/hooks/useMeechi.js +461 -0
- package/dist/hooks/useSync.d.ts +8 -0
- package/dist/hooks/useSync.js +87 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/lib/ai/embeddings.d.ts +15 -0
- package/dist/lib/ai/embeddings.js +128 -0
- package/dist/lib/ai/gpu-lock.d.ts +19 -0
- package/dist/lib/ai/gpu-lock.js +43 -0
- package/dist/lib/ai/llm.worker.d.ts +1 -0
- package/dist/lib/ai/llm.worker.js +7 -0
- package/dist/lib/ai/local-llm.d.ts +30 -0
- package/dist/lib/ai/local-llm.js +211 -0
- package/dist/lib/ai/manager.d.ts +20 -0
- package/dist/lib/ai/manager.js +51 -0
- package/dist/lib/ai/parsing.d.ts +12 -0
- package/dist/lib/ai/parsing.js +56 -0
- package/dist/lib/ai/prompts.d.ts +2 -0
- package/dist/lib/ai/prompts.js +2 -0
- package/dist/lib/ai/providers/gemini.d.ts +6 -0
- package/dist/lib/ai/providers/gemini.js +88 -0
- package/dist/lib/ai/providers/groq.d.ts +6 -0
- package/dist/lib/ai/providers/groq.js +42 -0
- package/dist/lib/ai/registry.d.ts +29 -0
- package/dist/lib/ai/registry.js +52 -0
- package/dist/lib/ai/tools.d.ts +2 -0
- package/dist/lib/ai/tools.js +106 -0
- package/dist/lib/ai/types.d.ts +22 -0
- package/dist/lib/ai/types.js +1 -0
- package/dist/lib/ai/worker.d.ts +1 -0
- package/dist/lib/ai/worker.js +60 -0
- package/dist/lib/audio/input.d.ts +13 -0
- package/dist/lib/audio/input.js +121 -0
- package/dist/lib/audio/stt.d.ts +13 -0
- package/dist/lib/audio/stt.js +119 -0
- package/dist/lib/audio/tts.d.ts +12 -0
- package/dist/lib/audio/tts.js +128 -0
- package/dist/lib/audio/vad.d.ts +18 -0
- package/dist/lib/audio/vad.js +117 -0
- package/dist/lib/colors.d.ts +16 -0
- package/dist/lib/colors.js +67 -0
- package/dist/lib/extensions.d.ts +35 -0
- package/dist/lib/extensions.js +24 -0
- package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
- package/dist/lib/hooks/use-voice-loop.js +313 -0
- package/dist/lib/mcp/McpClient.d.ts +19 -0
- package/dist/lib/mcp/McpClient.js +42 -0
- package/dist/lib/mcp/McpRegistry.d.ts +47 -0
- package/dist/lib/mcp/McpRegistry.js +117 -0
- package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
- package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
- package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
- package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
- package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
- package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
- package/dist/lib/mcp/native/index.d.ts +10 -0
- package/dist/lib/mcp/native/index.js +10 -0
- package/dist/lib/mcp/types.d.ts +35 -0
- package/dist/lib/mcp/types.js +1 -0
- package/dist/lib/pdf.d.ts +10 -0
- package/dist/lib/pdf.js +142 -0
- package/dist/lib/settings.d.ts +48 -0
- package/dist/lib/settings.js +87 -0
- package/dist/lib/storage/db.d.ts +57 -0
- package/dist/lib/storage/db.js +45 -0
- package/dist/lib/storage/local.d.ts +28 -0
- package/dist/lib/storage/local.js +534 -0
- package/dist/lib/storage/migrate.d.ts +3 -0
- package/dist/lib/storage/migrate.js +122 -0
- package/dist/lib/storage/types.d.ts +66 -0
- package/dist/lib/storage/types.js +1 -0
- package/dist/lib/sync/client-drive.d.ts +9 -0
- package/dist/lib/sync/client-drive.js +69 -0
- package/dist/lib/sync/engine.d.ts +18 -0
- package/dist/lib/sync/engine.js +517 -0
- package/dist/lib/sync/google-drive.d.ts +52 -0
- package/dist/lib/sync/google-drive.js +183 -0
- package/dist/lib/sync/merge.d.ts +1 -0
- package/dist/lib/sync/merge.js +68 -0
- package/dist/lib/yjs/YjsProvider.d.ts +11 -0
- package/dist/lib/yjs/YjsProvider.js +33 -0
- package/dist/lib/yjs/graph.d.ts +11 -0
- package/dist/lib/yjs/graph.js +7 -0
- package/dist/lib/yjs/hooks.d.ts +7 -0
- package/dist/lib/yjs/hooks.js +37 -0
- package/dist/lib/yjs/store.d.ts +4 -0
- package/dist/lib/yjs/store.js +19 -0
- package/dist/lib/yjs/syncGraph.d.ts +1 -0
- package/dist/lib/yjs/syncGraph.js +38 -0
- package/dist/providers/theme-provider.d.ts +3 -0
- package/dist/providers/theme-provider.js +18 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import styles from '../app/app/page.module.css';
|
|
5
|
+
import { useLiveQuery } from 'dexie-react-hooks';
|
|
6
|
+
import { db } from '../lib/storage/db';
|
|
7
|
+
import { extractTextFromPdf } from '../lib/pdf';
|
|
8
|
+
import Icon from './Icon';
|
|
9
|
+
import { extensions } from '../lib/extensions';
|
|
10
|
+
export default function FileExplorer(props) {
|
|
11
|
+
const { storage, onClose, onOpenFile } = props;
|
|
12
|
+
const [currentPath, setCurrentPath] = useState(() => {
|
|
13
|
+
if (typeof window !== 'undefined') {
|
|
14
|
+
return localStorage.getItem('meechi_explorer_path') || 'misc';
|
|
15
|
+
}
|
|
16
|
+
return 'misc';
|
|
17
|
+
});
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
localStorage.setItem('meechi_explorer_path', currentPath);
|
|
20
|
+
}, [currentPath]);
|
|
21
|
+
// Actions State
|
|
22
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
23
|
+
const fileInputRef = useRef(null);
|
|
24
|
+
const [activeMenuId, setActiveMenuId] = useState(null);
|
|
25
|
+
const [isBulkMode, setIsBulkMode] = useState(false);
|
|
26
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
27
|
+
// Close menu on click outside - Use CAPTURE to handle before React bubbles
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handleClickOutside = (e) => {
|
|
30
|
+
// Don't close if clicking the kebab button OR inside the menu (let item handlers manage closure)
|
|
31
|
+
if (e.target.closest(`.${styles.kebabButton}`) || e.target.closest(`.${styles.dropdownMenu}`))
|
|
32
|
+
return;
|
|
33
|
+
setActiveMenuId(null);
|
|
34
|
+
};
|
|
35
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
36
|
+
return () => document.removeEventListener('click', handleClickOutside, true);
|
|
37
|
+
}, []);
|
|
38
|
+
const lastInteractionIndex = useRef(-1);
|
|
39
|
+
const [dialogAction, setDialogAction] = useState(null);
|
|
40
|
+
// Link Dialog State
|
|
41
|
+
const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
|
|
42
|
+
const [linkUrl, setLinkUrl] = useState("");
|
|
43
|
+
const [isFetchingLink, setIsFetchingLink] = useState(false);
|
|
44
|
+
const showAlert = (title, message, onConfirm) => {
|
|
45
|
+
setDialogAction({ type: 'alert', title, message, onConfirm });
|
|
46
|
+
};
|
|
47
|
+
const handleAddLink = async () => {
|
|
48
|
+
if (!linkUrl)
|
|
49
|
+
return;
|
|
50
|
+
setIsFetchingLink(true);
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch('/api/utils/fetch-url', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
body: JSON.stringify({ url: linkUrl }),
|
|
55
|
+
headers: { 'Content-Type': 'application/json' }
|
|
56
|
+
});
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
if (!res.ok)
|
|
59
|
+
throw new Error(data.message);
|
|
60
|
+
// Create file
|
|
61
|
+
const safeTitle = data.title.replace(/[^a-zA-Z0-9 \-_]/g, '').trim() || 'Untitled Source';
|
|
62
|
+
const fileName = `${safeTitle}.source.md`;
|
|
63
|
+
const path = `${currentPath}/${fileName}`;
|
|
64
|
+
// Add Summary Wrapper if content is long?
|
|
65
|
+
// The prompt says: "create a md file with the parsed text, and summary in the beggining"
|
|
66
|
+
// Generate Summary
|
|
67
|
+
let finalContent = data.content;
|
|
68
|
+
try {
|
|
69
|
+
const sumRes = await fetch('/api/ai/summarize', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: JSON.stringify({ content: data.content }),
|
|
72
|
+
headers: { 'Content-Type': 'application/json' }
|
|
73
|
+
});
|
|
74
|
+
const sumData = await sumRes.json();
|
|
75
|
+
if (sumData.summary) {
|
|
76
|
+
finalContent = `> **Summary**: ${sumData.summary}\n\n> **Source**: ${linkUrl}\n\n---\n\n${data.content}`;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
finalContent = `> **Source**: ${linkUrl}\n\n---\n\n${data.content}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.error("Summary failed", e);
|
|
84
|
+
finalContent = `> **Source**: ${linkUrl}\n\n---\n\n${data.content}`;
|
|
85
|
+
}
|
|
86
|
+
await storage.saveFile(path, finalContent);
|
|
87
|
+
setIsLinkDialogOpen(false);
|
|
88
|
+
setLinkUrl("");
|
|
89
|
+
showAlert("Success", "Link added successfully!");
|
|
90
|
+
await storage.saveFile(path, finalContent);
|
|
91
|
+
setIsLinkDialogOpen(false);
|
|
92
|
+
setLinkUrl("");
|
|
93
|
+
showAlert("Success", "Link added successfully!");
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
showAlert("Error", "Failed to add link: " + e.message);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
setIsFetchingLink(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Initial Migration Trigger
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
storage.init().catch(err => console.error("Migration failed", err));
|
|
105
|
+
}, [storage]);
|
|
106
|
+
// REACTIVE DATA FETCHING (Dexie Magic 🪄)
|
|
107
|
+
const items = useLiveQuery(async () => {
|
|
108
|
+
// Dynamic Query based on path
|
|
109
|
+
// optimization: if path is "root", show top level folders?
|
|
110
|
+
// Our structure implies everything starts with "misc/" or "history/".
|
|
111
|
+
// Let's query EVERYTHING for now to ensure folders appear?
|
|
112
|
+
// No, querying everything (startsWith("")) is better for discovery if we don't strictly enforce 'misc' root.
|
|
113
|
+
let collection;
|
|
114
|
+
const isRoot = currentPath === 'root';
|
|
115
|
+
const queryPath = isRoot ? '' : currentPath;
|
|
116
|
+
// simple prefix query
|
|
117
|
+
const allFiles = await db.files.where('path').startsWith(queryPath).toArray();
|
|
118
|
+
console.log(`[FileExplorer] Query '${queryPath}' returned ${allFiles.length} files.`, allFiles.map(f => f.path));
|
|
119
|
+
const folders = new Set();
|
|
120
|
+
const currentLevelFiles = [];
|
|
121
|
+
allFiles.filter(f => !f.deleted).forEach(f => {
|
|
122
|
+
// Filter: Must start with currentPath + '/'
|
|
123
|
+
// Special case: if isRoot, just look for top level dirs?
|
|
124
|
+
let relative = "";
|
|
125
|
+
if (isRoot) {
|
|
126
|
+
relative = f.path;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
if (!f.path.startsWith(currentPath + '/'))
|
|
130
|
+
return;
|
|
131
|
+
relative = f.path.substring(currentPath.length + 1);
|
|
132
|
+
}
|
|
133
|
+
if (relative.includes('/')) {
|
|
134
|
+
folders.add(relative.split('/')[0]);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// It's a file right here
|
|
138
|
+
currentLevelFiles.push({
|
|
139
|
+
id: f.path,
|
|
140
|
+
name: f.path.split('/').pop() || f.path,
|
|
141
|
+
path: f.path,
|
|
142
|
+
updatedAt: f.updatedAt,
|
|
143
|
+
type: f.type,
|
|
144
|
+
remoteId: f.remoteId,
|
|
145
|
+
tags: f.tags,
|
|
146
|
+
metadata: f.metadata
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// NotebookLM Style: Group "Source" files
|
|
151
|
+
// If we have "foo.pdf" and "foo.pdf.source.md", hide "foo.pdf" and rename "foo.pdf.source.md" to "foo.pdf (Source)"
|
|
152
|
+
// 1. Find all sources
|
|
153
|
+
// 1. Find all sources and potential wrapper notes
|
|
154
|
+
const sources = currentLevelFiles.filter(f => f.name.endsWith('.source.md'));
|
|
155
|
+
const sourceMap = new Set(sources.map(s => s.name));
|
|
156
|
+
const allFileNames = new Set(currentLevelFiles.map(f => f.name));
|
|
157
|
+
// 2. Filter out raw PDFs if they have a source
|
|
158
|
+
const finalFiles = currentLevelFiles.filter(f => {
|
|
159
|
+
const isSource = f.name.endsWith('.source.md');
|
|
160
|
+
if (isSource)
|
|
161
|
+
return true;
|
|
162
|
+
const potentialSourceName = f.name + '.source.md';
|
|
163
|
+
if (sourceMap.has(potentialSourceName))
|
|
164
|
+
return false; // Hide raw PDF if Source exists
|
|
165
|
+
const potentialNoteName = f.name + '.md';
|
|
166
|
+
if (allFileNames.has(potentialNoteName))
|
|
167
|
+
return false; // Hide raw PDF if Converted Note exists
|
|
168
|
+
return true;
|
|
169
|
+
}).map(f => {
|
|
170
|
+
if (f.name.endsWith('.source.md')) {
|
|
171
|
+
return Object.assign(Object.assign({}, f), { name: f.name.replace('.pdf.source.md', ' (Source)'), type: 'source' // New visual type?
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return f;
|
|
175
|
+
});
|
|
176
|
+
const folderItems = Array.from(folders)
|
|
177
|
+
.filter(name => !currentLevelFiles.some(f => f.name === name && f.type === 'folder'))
|
|
178
|
+
.map(name => ({
|
|
179
|
+
id: isRoot ? name : `${currentPath}/${name}`,
|
|
180
|
+
name: name,
|
|
181
|
+
path: isRoot ? name : `${currentPath}/${name}`,
|
|
182
|
+
updatedAt: Date.now(),
|
|
183
|
+
type: 'folder'
|
|
184
|
+
}));
|
|
185
|
+
return [...folderItems, ...finalFiles].sort((a, b) => {
|
|
186
|
+
if (a.type === b.type)
|
|
187
|
+
return a.name.localeCompare(b.name);
|
|
188
|
+
return a.type === 'folder' ? -1 : 1;
|
|
189
|
+
});
|
|
190
|
+
}, [currentPath]) || [];
|
|
191
|
+
// Loading deleted (Dexie handles it)
|
|
192
|
+
// ... existing handlers ...
|
|
193
|
+
// Note: We no longer need to call loadFiles() manually after actions!
|
|
194
|
+
// saveFile -> DB Update -> useLiveQuery triggers -> UI Updates automatically.
|
|
195
|
+
const handleNavigate = (folderName) => {
|
|
196
|
+
setCurrentPath(`${currentPath}/${folderName}`);
|
|
197
|
+
};
|
|
198
|
+
const handleBack = () => {
|
|
199
|
+
if (currentPath === 'misc')
|
|
200
|
+
return;
|
|
201
|
+
const parts = currentPath.split('/');
|
|
202
|
+
parts.pop();
|
|
203
|
+
setCurrentPath(parts.join('/'));
|
|
204
|
+
};
|
|
205
|
+
const handleCreateFolder = async () => {
|
|
206
|
+
const name = prompt("Topic Name:");
|
|
207
|
+
if (!name)
|
|
208
|
+
return;
|
|
209
|
+
// Explicitly create folder record for sync
|
|
210
|
+
const path = `${currentPath}/${name}`;
|
|
211
|
+
await storage.createFolder(path);
|
|
212
|
+
};
|
|
213
|
+
const processFile = async (file, targetPath) => {
|
|
214
|
+
const path = `${targetPath}/${file.name}`;
|
|
215
|
+
let content = "";
|
|
216
|
+
if (file.type.startsWith('text/') || file.name.endsWith('.md') || file.name.endsWith('.txt')) {
|
|
217
|
+
content = await file.text();
|
|
218
|
+
}
|
|
219
|
+
else if (file.type === 'application/pdf') {
|
|
220
|
+
try {
|
|
221
|
+
const buffer = await file.arrayBuffer();
|
|
222
|
+
const extractedText = await extractTextFromPdf(buffer.slice(0));
|
|
223
|
+
let finalContent = `## Source: ${file.name}\n\n${extractedText}`;
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetch('/api/ai/summarize', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: JSON.stringify({ content: extractedText }),
|
|
228
|
+
headers: { 'Content-Type': 'application/json' }
|
|
229
|
+
});
|
|
230
|
+
const data = await res.json();
|
|
231
|
+
if (data.summary) {
|
|
232
|
+
finalContent = `> **Summary**: ${data.summary}\n\n---\n\n${finalContent}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
console.error("Auto-summary failed", e);
|
|
237
|
+
}
|
|
238
|
+
const sourcePath = `${path}.source.md`;
|
|
239
|
+
await storage.saveFile(sourcePath, finalContent);
|
|
240
|
+
await storage.saveFile(path, buffer);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.error("PDF Upload Trace", e);
|
|
245
|
+
alert("Failed to parse PDF");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
alert(`Skipped ${file.name}: Only text/markdown/pdf supported`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let finalContent = content;
|
|
254
|
+
try {
|
|
255
|
+
const res = await fetch('/api/ai/summarize', {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
body: JSON.stringify({ content }),
|
|
258
|
+
headers: { 'Content-Type': 'application/json' }
|
|
259
|
+
});
|
|
260
|
+
const data = await res.json();
|
|
261
|
+
if (data.summary) {
|
|
262
|
+
finalContent = `> **Summary**: ${data.summary}\n\n---\n\n${content}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
console.error("Auto-summary failed", e);
|
|
267
|
+
}
|
|
268
|
+
await storage.saveFile(path, finalContent);
|
|
269
|
+
};
|
|
270
|
+
const handleUpload = async (e) => {
|
|
271
|
+
var _a;
|
|
272
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
273
|
+
if (!file)
|
|
274
|
+
return;
|
|
275
|
+
await processFile(file, currentPath);
|
|
276
|
+
}; // loadFiles(); // Removed: Reactive
|
|
277
|
+
// Bulk Delete
|
|
278
|
+
const handleBulkDelete = async () => {
|
|
279
|
+
if (selectedIds.size === 0)
|
|
280
|
+
return;
|
|
281
|
+
if (!confirm(`Delete ${selectedIds.size} items?`))
|
|
282
|
+
return;
|
|
283
|
+
// Use db transaction for bulk delete?
|
|
284
|
+
// For now, loop.
|
|
285
|
+
for (const id of Array.from(selectedIds)) {
|
|
286
|
+
const item = items.find(i => i.id === id);
|
|
287
|
+
if (item)
|
|
288
|
+
await performDelete(item);
|
|
289
|
+
}
|
|
290
|
+
setSelectedIds(new Set()); // Clear selection
|
|
291
|
+
};
|
|
292
|
+
const handleDeleteClick = (file) => {
|
|
293
|
+
setDialogAction({ type: 'delete', file });
|
|
294
|
+
};
|
|
295
|
+
const confirmDelete = async () => {
|
|
296
|
+
if (!dialogAction || dialogAction.type !== 'delete')
|
|
297
|
+
return;
|
|
298
|
+
const file = dialogAction.file;
|
|
299
|
+
setDialogAction(null); // Close immediately
|
|
300
|
+
try {
|
|
301
|
+
await performDelete(file);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
console.error("Delete failed:", e);
|
|
305
|
+
alert("Delete Error: " + e.message);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const performDelete = async (file) => {
|
|
309
|
+
if (file.type === 'folder') {
|
|
310
|
+
const all = await storage.listFiles(file.path);
|
|
311
|
+
for (const f of all) {
|
|
312
|
+
await storage.deleteFile(f.path);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (file.type === 'source') {
|
|
316
|
+
// Delete Source AND Original PDF
|
|
317
|
+
await storage.deleteFile(file.path);
|
|
318
|
+
const originalPath = file.path.replace('.source.md', '');
|
|
319
|
+
try {
|
|
320
|
+
await storage.deleteFile(originalPath);
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
console.warn("Could not delete original PDF (might not exist):", e);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
await storage.deleteFile(file.path);
|
|
328
|
+
// Also check if there's a source for this file (if we deleted the raw PDF manually?)
|
|
329
|
+
// Usually we hide raw PDF, but if we delete from search results or something.
|
|
330
|
+
// Safe to check.
|
|
331
|
+
if (file.path.endsWith('.pdf')) {
|
|
332
|
+
const sourcePath = `${file.path}.source.md`;
|
|
333
|
+
await storage.deleteFile(sourcePath);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const handleRenameClick = (file) => {
|
|
338
|
+
setDialogAction({
|
|
339
|
+
type: 'rename',
|
|
340
|
+
file,
|
|
341
|
+
value: file.type === 'source' ? file.name.replace(' (Source)', '') : file.name
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
const confirmRename = async () => {
|
|
345
|
+
if (!dialogAction || dialogAction.type !== 'rename')
|
|
346
|
+
return;
|
|
347
|
+
const { file, value: newName } = dialogAction;
|
|
348
|
+
setDialogAction(null);
|
|
349
|
+
if (!newName || newName === file.name)
|
|
350
|
+
return;
|
|
351
|
+
// Handle Source Rename
|
|
352
|
+
if (file.type === 'source') {
|
|
353
|
+
const oldSourcePath = file.path;
|
|
354
|
+
const oldPdfPath = oldSourcePath.replace('.source.md', '');
|
|
355
|
+
const oldPdfName = oldPdfPath.split('/').pop() || '';
|
|
356
|
+
let newPdfName = newName;
|
|
357
|
+
if (!newPdfName.endsWith('.pdf'))
|
|
358
|
+
newPdfName += '.pdf';
|
|
359
|
+
if (newPdfName === oldPdfName)
|
|
360
|
+
return;
|
|
361
|
+
const parentDir = oldPdfPath.substring(0, oldPdfPath.lastIndexOf('/'));
|
|
362
|
+
const newPdfPath = `${parentDir}/${newPdfName}`;
|
|
363
|
+
const newSourcePath = `${newPdfPath}.source.md`;
|
|
364
|
+
try {
|
|
365
|
+
// Try rename PDF first
|
|
366
|
+
try {
|
|
367
|
+
await storage.renameFile(oldPdfPath, newPdfPath);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
console.warn("Could not rename original PDF (might not exist):", err);
|
|
371
|
+
}
|
|
372
|
+
// Always rename Source
|
|
373
|
+
await storage.renameFile(oldSourcePath, newSourcePath);
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
alert("Rename failed: " + e.message);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const oldPath = file.path;
|
|
381
|
+
const parts = oldPath.split('/');
|
|
382
|
+
parts.pop();
|
|
383
|
+
const newPath = `${parts.join('/')}/${newName}`;
|
|
384
|
+
try {
|
|
385
|
+
await storage.renameFile(oldPath, newPath);
|
|
386
|
+
}
|
|
387
|
+
catch (e) {
|
|
388
|
+
alert("Rename failed: " + e.message);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const handleToggleSourceStatus = async (file) => {
|
|
392
|
+
try {
|
|
393
|
+
// Fetch FRESH metadata from storage to ensure we don't lose 'edited' status
|
|
394
|
+
const freshFile = await storage.getFile(file.path);
|
|
395
|
+
if (!freshFile) {
|
|
396
|
+
showAlert("Error", "File not found locally.");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const currentMeta = freshFile.metadata || {};
|
|
400
|
+
if (file.type === 'source') {
|
|
401
|
+
// Convert to Note: Rename .source.md -> .md
|
|
402
|
+
const newPath = file.path.replace('.source.md', '.md');
|
|
403
|
+
await storage.renameFile(file.path, newPath);
|
|
404
|
+
// Preserve existing metadata (including 'edited'), just flip isSource
|
|
405
|
+
await storage.updateMetadata(newPath, { type: 'file', metadata: Object.assign(Object.assign({}, currentMeta), { isSource: false }) });
|
|
406
|
+
showAlert("Success", `Converted "${file.name}" to Note.`);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
// Convert to Source: Rename .md -> .source.md
|
|
410
|
+
let newPath = file.path;
|
|
411
|
+
if (!newPath.endsWith('.source.md')) {
|
|
412
|
+
if (newPath.endsWith('.md')) {
|
|
413
|
+
newPath = newPath.replace(/\.md$/, '.source.md');
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
newPath = newPath + '.source.md'; // Fallback
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await storage.renameFile(file.path, newPath);
|
|
420
|
+
await storage.updateMetadata(newPath, { type: 'source', metadata: Object.assign(Object.assign({}, currentMeta), { isSource: true }) });
|
|
421
|
+
showAlert("Success", `Converted "${file.name}" to Source.`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
console.error(e);
|
|
426
|
+
showAlert("Error", "Conversion failed: " + e.message);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const handleResetSource = async (file) => {
|
|
430
|
+
if (file.type !== 'source')
|
|
431
|
+
return;
|
|
432
|
+
const performReset = async () => {
|
|
433
|
+
setDialogAction(null);
|
|
434
|
+
try {
|
|
435
|
+
let pdfContent;
|
|
436
|
+
const originalPath = file.path.replace('.source.md', '');
|
|
437
|
+
const rawFile = await storage.readFile(originalPath);
|
|
438
|
+
if (!rawFile)
|
|
439
|
+
throw new Error("Original PDF file not found. Cannot reset.");
|
|
440
|
+
if (rawFile instanceof ArrayBuffer) {
|
|
441
|
+
pdfContent = rawFile;
|
|
442
|
+
}
|
|
443
|
+
else if (typeof rawFile === 'string') {
|
|
444
|
+
throw new Error("Expected binary PDF, got text.");
|
|
445
|
+
}
|
|
446
|
+
if (!pdfContent)
|
|
447
|
+
throw new Error("Could not read binary content.");
|
|
448
|
+
let content = await extractTextFromPdf(pdfContent);
|
|
449
|
+
const displayName = file.name.replace(' (Source)', '');
|
|
450
|
+
content = `## Source: ${displayName}\n\n${content}`;
|
|
451
|
+
const meta = file.metadata || {};
|
|
452
|
+
const newMeta = Object.assign(Object.assign({}, meta), { edited: false, comments: [] });
|
|
453
|
+
await storage.saveFile(file.path, content, undefined, file.tags, newMeta);
|
|
454
|
+
showAlert("Success", "Source reset to original content.");
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
console.error("Reset failed", e);
|
|
458
|
+
showAlert("Error", "Reset failed: " + e.message);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
setDialogAction({
|
|
462
|
+
type: 'confirm',
|
|
463
|
+
title: 'Reset Source',
|
|
464
|
+
message: `Reset "${file.name}" to original? This will discard all edits and re-extract text from the PDF.`,
|
|
465
|
+
confirmLabel: 'Reset',
|
|
466
|
+
onConfirm: performReset
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
const handleSelectionClick = (item, index, event) => {
|
|
470
|
+
event.stopPropagation();
|
|
471
|
+
const newSelected = new Set(selectedIds);
|
|
472
|
+
if (event.shiftKey && lastInteractionIndex.current !== -1) {
|
|
473
|
+
// Range Select
|
|
474
|
+
const start = Math.min(lastInteractionIndex.current, index);
|
|
475
|
+
const end = Math.max(lastInteractionIndex.current, index);
|
|
476
|
+
for (let i = start; i <= end; i++) {
|
|
477
|
+
newSelected.add(items[i].id);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (event.ctrlKey || event.metaKey) {
|
|
481
|
+
// Toggle
|
|
482
|
+
if (newSelected.has(item.id))
|
|
483
|
+
newSelected.delete(item.id);
|
|
484
|
+
else
|
|
485
|
+
newSelected.add(item.id);
|
|
486
|
+
lastInteractionIndex.current = index;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Single Select (clears others) - Standard Logic
|
|
490
|
+
// If user just clicks a checkbox, they usually expect "Add to selection" or "Toggle"
|
|
491
|
+
// But if they click the *row*, they expect "Select Only This".
|
|
492
|
+
// Since this is triggered by the checkbox click (mostly), let's keep it as Toggle logic if strictly checking box?
|
|
493
|
+
// Actually, Windows Explorer: Checkbox click = Toggle. Row Click = Select Only This.
|
|
494
|
+
// Let's assume this handles the Checkbox Click for now.
|
|
495
|
+
if (newSelected.has(item.id))
|
|
496
|
+
newSelected.delete(item.id);
|
|
497
|
+
else
|
|
498
|
+
newSelected.add(item.id);
|
|
499
|
+
lastInteractionIndex.current = index;
|
|
500
|
+
}
|
|
501
|
+
setSelectedIds(newSelected);
|
|
502
|
+
};
|
|
503
|
+
const handleRowClick = (item, index, event) => {
|
|
504
|
+
// Desktop Standard: Row Click = Select Only This (unless Cmd/Ctrl/Shift)
|
|
505
|
+
const newSelected = new Set(selectedIds);
|
|
506
|
+
if (event.shiftKey && lastInteractionIndex.current !== -1) {
|
|
507
|
+
const start = Math.min(lastInteractionIndex.current, index);
|
|
508
|
+
const end = Math.max(lastInteractionIndex.current, index);
|
|
509
|
+
// Clear prior if Shift click? Usually Shift+Click keeps others?
|
|
510
|
+
// Standard: Shift+Click extends selection from anchor.
|
|
511
|
+
// Simplified: Add range.
|
|
512
|
+
for (let i = start; i <= end; i++) {
|
|
513
|
+
newSelected.add(items[i].id);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (event.ctrlKey || event.metaKey) {
|
|
517
|
+
if (newSelected.has(item.id))
|
|
518
|
+
newSelected.delete(item.id);
|
|
519
|
+
else
|
|
520
|
+
newSelected.add(item.id);
|
|
521
|
+
lastInteractionIndex.current = index;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// Single Click -> Select ONLY this
|
|
525
|
+
newSelected.clear();
|
|
526
|
+
newSelected.add(item.id);
|
|
527
|
+
lastInteractionIndex.current = index;
|
|
528
|
+
}
|
|
529
|
+
setSelectedIds(newSelected);
|
|
530
|
+
};
|
|
531
|
+
const handleSelectAll = () => {
|
|
532
|
+
if (selectedIds.size === items.length) {
|
|
533
|
+
setSelectedIds(new Set());
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
setSelectedIds(new Set(items.map(i => i.id)));
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const toggleSelect = (id) => {
|
|
540
|
+
const next = new Set(selectedIds);
|
|
541
|
+
if (next.has(id))
|
|
542
|
+
next.delete(id);
|
|
543
|
+
else
|
|
544
|
+
next.add(id);
|
|
545
|
+
setSelectedIds(next);
|
|
546
|
+
};
|
|
547
|
+
// DRAG AND DROP
|
|
548
|
+
const handleDragStart = (e, item) => {
|
|
549
|
+
// ... (lines 258-268)
|
|
550
|
+
// If the item is in current selection, drag ALL selected
|
|
551
|
+
// Else just drag this one
|
|
552
|
+
let dragIds = [item.id];
|
|
553
|
+
if (selectedIds.has(item.id)) {
|
|
554
|
+
dragIds = Array.from(selectedIds);
|
|
555
|
+
}
|
|
556
|
+
e.dataTransfer.setData('application/json', JSON.stringify(dragIds));
|
|
557
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
558
|
+
};
|
|
559
|
+
const handleDragOver = (e) => {
|
|
560
|
+
e.preventDefault(); // Allow drop
|
|
561
|
+
e.dataTransfer.dropEffect = 'move';
|
|
562
|
+
};
|
|
563
|
+
const handleDrop = async (e, targetFolder) => {
|
|
564
|
+
e.preventDefault();
|
|
565
|
+
e.stopPropagation();
|
|
566
|
+
if (targetFolder.type !== 'folder')
|
|
567
|
+
return;
|
|
568
|
+
const raw = e.dataTransfer.getData('application/json');
|
|
569
|
+
// 1. External File Drop
|
|
570
|
+
if (!raw) {
|
|
571
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
572
|
+
const files = Array.from(e.dataTransfer.files);
|
|
573
|
+
for (const file of files) {
|
|
574
|
+
await processFile(file, targetFolder.path);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// 2. Internal Move
|
|
580
|
+
const ids = JSON.parse(raw);
|
|
581
|
+
// Filter out if trying to drop into self
|
|
582
|
+
if (ids.includes(targetFolder.id))
|
|
583
|
+
return;
|
|
584
|
+
// Move Logic
|
|
585
|
+
for (const id of ids) {
|
|
586
|
+
const item = items.find(i => i.id === id);
|
|
587
|
+
if (!item)
|
|
588
|
+
continue;
|
|
589
|
+
// Folder Move or File Move?
|
|
590
|
+
// Simplified: We assume flat structure support for now in `renameFile`
|
|
591
|
+
// Old Path: misc/A.txt
|
|
592
|
+
// New Path: misc/Folder/A.txt
|
|
593
|
+
const newPath = `${targetFolder.path}/${item.name}`;
|
|
594
|
+
try {
|
|
595
|
+
// If it's a folder, we need recursive move?
|
|
596
|
+
// IndexedDB 'folder' is virtual.
|
|
597
|
+
// We actually need to find ALL files starting with item.path and replace prefix.
|
|
598
|
+
// Does storage.renameFile support directory rename?
|
|
599
|
+
// Our implementation in local.ts handles Single File rename.
|
|
600
|
+
// We need to upgrade performMove to handle folders.
|
|
601
|
+
await performMove(item, newPath);
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
console.error("Move failed", err);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// loadFiles(); // Removed: Reactive
|
|
608
|
+
};
|
|
609
|
+
const performMove = async (item, newPath) => {
|
|
610
|
+
if (item.type === 'file') {
|
|
611
|
+
await storage.renameFile(item.path, newPath);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
// Folder Move: Rename prefix for all children
|
|
615
|
+
// Get all files with prefix `item.path/`
|
|
616
|
+
// e.g. misc/OldFolder/... -> misc/NewFolder/...
|
|
617
|
+
const allChilds = await storage.listFiles(item.path); // uses startsWith
|
|
618
|
+
// Target Folder Path is derived from newPath (which includes the folder name)
|
|
619
|
+
// item.path = misc/OldFolder
|
|
620
|
+
// newPath = misc/Target/OldFolder
|
|
621
|
+
for (const child of allChilds) {
|
|
622
|
+
const relative = child.path.substring(item.path.length); // /file.txt
|
|
623
|
+
const childNewPath = `${newPath}${relative}`;
|
|
624
|
+
await storage.renameFile(child.path, childNewPath);
|
|
625
|
+
}
|
|
626
|
+
// If local.ts implementation of renameFile handles copy+delete, this works.
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
return (_jsx("div", { className: styles.modalOverlay, onClick: onClose, children: _jsxs("div", { className: styles.modalContent, onClick: e => e.stopPropagation(), style: { width: '80%', maxWidth: 800, height: '80vh', display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }, children: [_jsx("h2", { style: { fontSize: '1.5rem', fontWeight: 600 }, children: "Topic Explorer" }), _jsx("button", { onClick: onClose, style: { background: 'none', border: 'none', fontSize: '1.5rem', cursor: 'pointer', color: 'var(--foreground)' }, children: "\u00D7" })] }), _jsxs("div", { style: { display: 'flex', gap: '1rem', marginBottom: '1rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border)', alignItems: 'center' }, children: [_jsxs("button", { onClick: handleBack, disabled: currentPath === 'misc', style: {
|
|
630
|
+
cursor: currentPath === 'misc' ? 'default' : 'pointer',
|
|
631
|
+
opacity: currentPath === 'misc' ? 0 : 1,
|
|
632
|
+
background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4,
|
|
633
|
+
color: 'var(--foreground)'
|
|
634
|
+
}, children: [_jsx(Icon, { name: "ArrowLeft", size: 18 }), " Back"] }), _jsxs("button", { onClick: () => {
|
|
635
|
+
setIsBulkMode(!isBulkMode);
|
|
636
|
+
if (isBulkMode)
|
|
637
|
+
setSelectedIds(new Set());
|
|
638
|
+
}, style: {
|
|
639
|
+
cursor: 'pointer',
|
|
640
|
+
background: isBulkMode ? 'var(--info)' : 'transparent',
|
|
641
|
+
color: isBulkMode ? 'var(--surface)' : 'var(--foreground)',
|
|
642
|
+
border: '1px solid var(--border)',
|
|
643
|
+
borderRadius: 4, padding: '4px 8px', display: 'flex', alignItems: 'center', gap: 4
|
|
644
|
+
}, children: [isBulkMode ? _jsx(Icon, { name: "CheckSquare", size: 16 }) : _jsx(Icon, { name: "Square", size: 16 }), " Select"] }), isBulkMode && (_jsx("button", { onClick: handleSelectAll, style: { cursor: 'pointer' }, children: selectedIds.size === items.length && items.length > 0 ? 'Deselect All' : 'Select All' })), isBulkMode && selectedIds.size > 0 && (_jsxs("button", { onClick: handleBulkDelete, style: { color: 'var(--error)', cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Icon, { name: "Trash2", size: 16 }), " Delete (", selectedIds.size, ")"] })), _jsx("div", { style: { flex: 1 } }), _jsx("button", { onClick: async () => {
|
|
645
|
+
if (confirm("Reset connection to Google Drive? This will re-upload all files to 'Meechi Journal'.")) {
|
|
646
|
+
await storage.resetSyncState();
|
|
647
|
+
alert("Sync Reset. Please Sign Out and Sign In again to refresh permissions.");
|
|
648
|
+
}
|
|
649
|
+
}, style: { cursor: 'pointer', marginRight: '1rem', color: 'var(--error)', border: '1px solid var(--error)', background: 'transparent', borderRadius: 4, padding: '4px 8px', fontSize: '0.8rem' }, children: "Reset Cloud" }), props.syncLogs && (_jsxs("button", { onClick: async () => {
|
|
650
|
+
if (storage.forceSync) {
|
|
651
|
+
await storage.forceSync();
|
|
652
|
+
await storage.forceSync();
|
|
653
|
+
await storage.forceSync(); // Triple sync hack
|
|
654
|
+
showAlert("Sync", "Sync triggered!");
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
showAlert("Sync", "Sync not available");
|
|
658
|
+
}
|
|
659
|
+
}, style: { cursor: 'pointer', marginRight: '1rem', color: 'var(--info)', border: '1px solid var(--info)', background: 'transparent', borderRadius: 4, padding: '4px 8px', display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Icon, { name: "RefreshCw", size: 14 }), " Sync Now"] })), _jsxs("button", { onClick: handleCreateFolder, style: { cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4, color: 'var(--foreground)' }, children: [_jsx(Icon, { name: "FolderPlus", size: 18 }), " New Topic"] }), _jsxs("button", { onClick: () => setIsLinkDialogOpen(true), style: { cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4, color: 'var(--foreground)' }, children: [_jsx(Icon, { name: "Link", size: 18 }), " Add Link"] }), _jsxs("button", { onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, style: { cursor: 'pointer', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 4, color: 'var(--foreground)' }, children: [_jsx(Icon, { name: "Upload", size: 18 }), " Upload Logic"] }), _jsx("input", { type: "file", ref: fileInputRef, onChange: handleUpload, style: { display: 'none' } })] }), _jsx("div", { style: { padding: '0.5rem', background: 'var(--background)', border: '1px solid var(--border)', borderRadius: 4, marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--secondary)', display: 'flex', gap: '0.5rem' }, children: currentPath.split('/').map((part, index, arr) => {
|
|
660
|
+
// ... breadcrumb logic ...
|
|
661
|
+
const pathSoFar = arr.slice(0, index + 1).join('/');
|
|
662
|
+
const isLast = index === arr.length - 1;
|
|
663
|
+
const targetFolder = { id: pathSoFar, name: part, path: pathSoFar, updatedAt: Date.now(), type: 'folder' };
|
|
664
|
+
return (_jsxs(React.Fragment, { children: [_jsx("span", { onDragOver: (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }, onDrop: (e) => handleDrop(e, targetFolder), onClick: () => !isLast && setCurrentPath(pathSoFar), style: {
|
|
665
|
+
cursor: isLast ? 'default' : 'pointer',
|
|
666
|
+
fontWeight: isLast ? 600 : 400,
|
|
667
|
+
textDecoration: isLast ? 'none' : 'underline'
|
|
668
|
+
}, children: part === 'misc' ? 'Home' : part }), !isLast && _jsx(Icon, { name: "ChevronRight", size: 14, style: { color: 'var(--muted)' } })] }, pathSoFar));
|
|
669
|
+
}) }), (dialogAction || isLinkDialogOpen) && (_jsx("div", { style: {
|
|
670
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
671
|
+
background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
672
|
+
zIndex: 1000
|
|
673
|
+
}, onClick: (e) => { e.stopPropagation(); setDialogAction(null); setIsLinkDialogOpen(false); }, children: _jsxs("div", { style: { background: 'var(--surface)', color: 'var(--foreground)', padding: '1.5rem', borderRadius: 8, width: 300, border: '1px solid var(--border)' }, onClick: e => e.stopPropagation(), children: [isLinkDialogOpen && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Add Link Source" }), _jsx("input", { autoFocus: true, placeholder: "https://example.com/article", value: linkUrl, onChange: e => setLinkUrl(e.target.value), onKeyDown: e => e.key === 'Enter' && handleAddLink(), style: { width: '100%', padding: '0.5rem', marginBottom: '1.5rem' } }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setIsLinkDialogOpen(false), style: { color: 'var(--foreground)', background: 'none', border: '1px solid var(--border)', padding: '0.5rem 1rem', borderRadius: 4 }, children: "Cancel" }), _jsx("button", { onClick: handleAddLink, disabled: isFetchingLink, style: { background: 'var(--accent)', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: isFetchingLink ? 'Fetching...' : 'Add' })] })] })), dialogAction && dialogAction.type === 'delete' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Confirm Delete" }), _jsxs("p", { style: { marginBottom: '1.5rem' }, children: ["Delete ", _jsx("b", { children: dialogAction.file.name }), "?"] }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setDialogAction(null), children: "Cancel" }), _jsx("button", { onClick: confirmDelete, style: { background: '#ff4444', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: "Delete" })] })] })), dialogAction && dialogAction.type === 'alert' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: dialogAction.title }), _jsx("p", { style: { marginBottom: '1.5rem', whiteSpace: 'pre-wrap' }, children: dialogAction.message }), _jsx("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: _jsx("button", { onClick: () => {
|
|
674
|
+
if (dialogAction.onConfirm)
|
|
675
|
+
dialogAction.onConfirm();
|
|
676
|
+
setDialogAction(null);
|
|
677
|
+
}, style: { background: '#007bff', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: "OK" }) })] })), dialogAction && dialogAction.type === 'confirm' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: dialogAction.title }), _jsx("p", { style: { marginBottom: '1.5rem', whiteSpace: 'pre-wrap' }, children: dialogAction.message }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setDialogAction(null), children: "Cancel" }), _jsx("button", { onClick: () => {
|
|
678
|
+
dialogAction.onConfirm();
|
|
679
|
+
// Dialog close handling is up to the caller usually?
|
|
680
|
+
// But for consistent UI, we might want to auto-close if the caller doesn't?
|
|
681
|
+
// The caller of confirm usually sets state.
|
|
682
|
+
// But looking at performSummarise logic I added "setDialogAction(null)" inside.
|
|
683
|
+
// However, let's keep it safe.
|
|
684
|
+
}, style: { background: '#007bff', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: dialogAction.confirmLabel || 'Confirm' })] })] })), dialogAction && dialogAction.type === 'rename' && (_jsxs(_Fragment, { children: [_jsx("h3", { style: { margin: '0 0 1rem 0' }, children: "Rename File" }), _jsx("input", { autoFocus: true, value: dialogAction.value, onChange: e => setDialogAction(Object.assign(Object.assign({}, dialogAction), { value: e.target.value })), onKeyDown: e => e.key === 'Enter' && confirmRename(), style: { width: '100%', padding: '0.5rem', marginBottom: '1.5rem' } }), _jsxs("div", { style: { display: 'flex', gap: '1rem', justifyContent: 'flex-end' }, children: [_jsx("button", { onClick: () => setDialogAction(null), children: "Cancel" }), _jsx("button", { onClick: confirmRename, style: { background: '#007bff', color: 'white', border: 'none', padding: '0.5rem 1rem', borderRadius: 4 }, children: "Rename" })] })] }))] }) })), _jsxs("div", { style: { flex: 1, overflowY: 'auto' }, onDragOver: (e) => {
|
|
685
|
+
e.preventDefault();
|
|
686
|
+
e.dataTransfer.dropEffect = 'move';
|
|
687
|
+
}, onDrop: async (e) => {
|
|
688
|
+
e.preventDefault();
|
|
689
|
+
// Target is currentPath
|
|
690
|
+
await handleDrop(e, { id: currentPath, name: currentPath.split('/').pop() || 'root', path: currentPath, type: 'folder', updatedAt: 0 });
|
|
691
|
+
}, children: [items.length === 0 && _jsx("div", { style: { textAlign: 'center', color: '#999', padding: '2rem' }, children: "Empty Topic (Drop files here)" }), items.map((item, index) => {
|
|
692
|
+
var _a, _b;
|
|
693
|
+
return (_jsxs("div", { className: `${styles.fileRow} ${selectedIds.has(item.id) ? styles.selected : ''}`, draggable: true, onDragStart: (e) => handleDragStart(e, item), onDragOver: (e) => item.type === 'folder' ? handleDragOver(e) : undefined, onDrop: (e) => item.type === 'folder' ? handleDrop(e, item) : undefined, onClick: (e) => handleRowClick(item, index, e), style: {
|
|
694
|
+
background: selectedIds.has(item.id) ? 'var(--accent)' : undefined,
|
|
695
|
+
color: selectedIds.has(item.id) ? '#fff' : undefined, // Force white text on accent
|
|
696
|
+
borderRadius: 6
|
|
697
|
+
}, onDoubleClick: () => {
|
|
698
|
+
if (item.type === 'folder') {
|
|
699
|
+
handleNavigate(item.name);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
// Open file
|
|
703
|
+
if (onOpenFile) {
|
|
704
|
+
onOpenFile(item.path);
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
onClose();
|
|
708
|
+
window.open(`/q?file=${encodeURIComponent(item.path)}`, '_self');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '1rem', flex: 1 }, children: [isBulkMode && (_jsx("input", { type: "checkbox", checked: selectedIds.has(item.id), readOnly: true, onClick: (e) => handleSelectionClick(item, index, e) })), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '1rem', flex: 1 }, children: [_jsx("span", { style: { color: selectedIds.has(item.id) ? 'currentColor' : '#666', display: 'flex', alignItems: 'center' }, children: item.type === 'folder' ?
|
|
712
|
+
_jsx(Icon, { name: "Folder", size: 20, fill: "currentColor", fillOpacity: selectedIds.has(item.id) ? 0.3 : 0.2 }) :
|
|
713
|
+
(item.type === 'source' ?
|
|
714
|
+
(((_a = item.metadata) === null || _a === void 0 ? void 0 : _a.edited) ? _jsx(Icon, { name: "BookOpen", size: 20 }) : _jsx(Icon, { name: "Book", size: 20 })) :
|
|
715
|
+
_jsx(Icon, { name: "FileText", size: 20 })) }), _jsx("span", { style: { fontWeight: item.type === 'folder' ? 600 : 400 }, children: item.name })] })] }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { className: `${styles.kebabButton} ${activeMenuId === item.id ? styles.active : ''}`, onClick: (e) => { e.stopPropagation(); setActiveMenuId(activeMenuId === item.id ? null : item.id); }, children: _jsx(Icon, { name: "MoreVertical", size: 16 }) }), activeMenuId === item.id && (_jsxs("div", { className: styles.dropdownMenu, onClick: e => e.stopPropagation(), style: { left: 'auto', right: 0 }, children: [_jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleRenameClick(item); }, children: "Rename" }), extensions.getFileActions().map(action => ((!action.shouldShow || action.shouldShow(item)) && (_jsxs("button", { className: styles.dropdownItem, onClick: async () => {
|
|
716
|
+
setActiveMenuId(null);
|
|
717
|
+
try {
|
|
718
|
+
await action.handler(item, { storage });
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
showAlert("Error", err.message || String(err));
|
|
722
|
+
}
|
|
723
|
+
}, children: [action.icon && _jsx(Icon, { name: action.icon.name, size: 16, style: { marginRight: 8 } }), action.label] }, action.id)))), item.type === 'source' ? (_jsxs(_Fragment, { children: [((_b = item.metadata) === null || _b === void 0 ? void 0 : _b.edited) && (_jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleResetSource(item); }, children: "Reset Source" })), _jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleToggleSourceStatus(item); }, children: "Convert to Note" })] })) : (_jsx("button", { className: styles.dropdownItem, onClick: () => { setActiveMenuId(null); handleToggleSourceStatus(item); }, children: "Make Source" })), _jsx("button", { className: `${styles.dropdownItem} ${styles.delete}`, onClick: () => { setActiveMenuId(null); handleDeleteClick(item); }, children: "Delete" })] }))] })] }, item.id));
|
|
724
|
+
})] }), _jsxs("div", { style: { marginTop: 'auto', borderTop: '1px solid #eee', paddingTop: '1rem' }, children: [_jsxs("div", { style: { marginBottom: '1rem' }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '5px' }, children: [_jsx("h4", { style: { margin: 0, fontSize: '0.8rem', color: '#666' }, children: "Live Sync Log" }), _jsx("button", { onClick: () => navigator.clipboard.writeText((props.syncLogs || []).join('\n')), style: { border: 'none', background: 'none', color: '#007bff', cursor: 'pointer', fontSize: '0.7rem' }, children: "Copy" })] }), _jsxs("div", { style: {
|
|
725
|
+
background: '#f8f8f8',
|
|
726
|
+
padding: '8px',
|
|
727
|
+
borderRadius: '4px',
|
|
728
|
+
height: '100px',
|
|
729
|
+
overflowY: 'auto',
|
|
730
|
+
fontSize: '0.7rem',
|
|
731
|
+
fontFamily: 'monospace',
|
|
732
|
+
border: '1px solid #eee',
|
|
733
|
+
display: 'flex', flexDirection: 'column', gap: '2px'
|
|
734
|
+
}, children: [(props.syncLogs || []).length === 0 && _jsx("span", { style: { color: '#999', fontStyle: 'italic' }, children: "Waiting for logs..." }), (props.syncLogs || []).slice().reverse().map((log, i) => (_jsx("div", { style: { borderBottom: '1px solid #f0f0f0' }, children: log }, i)))] })] }), _jsxs("details", { children: [_jsx("summary", { style: { cursor: 'pointer', color: '#666', fontSize: '0.85rem' }, children: "Storage Maintenance" }), _jsx("div", { style: { padding: '1rem', background: '#f9f9f9', borderRadius: 4, marginTop: '0.5rem', border: '1px solid #eee' }, children: _jsxs("div", { style: { display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }, children: [_jsx("button", { onClick: async () => {
|
|
735
|
+
const all = await db.files.count();
|
|
736
|
+
const deleted = await db.files.where('deleted').equals(1).count();
|
|
737
|
+
const dirty = await db.files.where('dirty').equals(1).count();
|
|
738
|
+
showAlert("Stats", `Total: ${all}\nTrash: ${deleted}\nUnsynced: ${dirty}`);
|
|
739
|
+
}, style: { padding: '4px 8px' }, children: "Check Stats" }), _jsx("button", { onClick: async () => {
|
|
740
|
+
if (confirm("Restore ALL items from trash?")) {
|
|
741
|
+
await db.transaction('rw', db.files, async () => {
|
|
742
|
+
await db.files.where('deleted').equals(1).modify({ deleted: 0, dirty: 1 });
|
|
743
|
+
});
|
|
744
|
+
alert("Items restored.");
|
|
745
|
+
}
|
|
746
|
+
}, style: { padding: '4px 8px' }, children: "Empty Trash" }), _jsx("button", { onClick: async () => {
|
|
747
|
+
if (confirm("PERMANENTLY delete all items in trash?")) {
|
|
748
|
+
await db.files.where('deleted').equals(1).delete();
|
|
749
|
+
alert("Trash purged.");
|
|
750
|
+
}
|
|
751
|
+
}, style: { padding: '4px 8px', color: 'red' }, children: "Purge Trash" }), _jsx("button", { onClick: async () => {
|
|
752
|
+
if (prompt("Type 'DELETE' to confirm Factory Reset. This wipes ALL local data.") === 'DELETE') {
|
|
753
|
+
await storage.factoryReset();
|
|
754
|
+
window.location.reload();
|
|
755
|
+
}
|
|
756
|
+
}, style: { padding: '4px 8px', color: 'white', background: 'red', fontWeight: 'bold' }, children: "FACTORY RESET" })] }) })] })] })] }) }));
|
|
757
|
+
}
|