fix: FilesTab empty state, button styles, text preview, menu positioning
- Add reactive $: block in FilesTab to auto-load files on selectedNode change - Copy .btn/.btn-primary/.btn-sm styles into NotesTab.svelte and FilesTab.svelte (scoped CSS) - Fix FilePreviewModal: render .md with MarkdownPreview, text files in <pre><code>, external open in footer - Expand codeNames/textMimes in fileUtils.js to cover all text file types - Add fallback text read for unknown file types in _openPreview - Fix toggleMenu in FileTreeRow: position menu near button using getBoundingClientRect - Add debug logs for file open flow and menu positioning - Add tabindex+keydown to MarkdownPreview div (a11y partial fix) Co-Authored-By: OWL (Hermes Agent) <hermes@nousresearch.com>
This commit is contained in:
@@ -88,7 +88,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
function toggleMenu(e) {
|
||||
if (e) {
|
||||
e.stopPropagation()
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
menuX = Math.min(rect.right, window.innerWidth - 240)
|
||||
menuY = Math.min(rect.bottom, window.innerHeight - 320)
|
||||
console.log('[FileTreeRow] menu source=button x=' + menuX + ' y=' + menuY)
|
||||
}
|
||||
menuOpen = !menuOpen
|
||||
}
|
||||
|
||||
@@ -112,6 +119,7 @@
|
||||
e.preventDefault()
|
||||
menuX = Math.min(e.clientX, window.innerWidth - 240)
|
||||
menuY = Math.min(e.clientY, window.innerHeight - 320)
|
||||
console.log('[FileTreeRow] menu source=contextmenu x=' + menuX + ' y=' + menuY)
|
||||
menuOpen = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
|
||||
import FileIcon from './FileIcon.svelte'
|
||||
import MarkdownPreview from './components/notes/MarkdownPreview.svelte'
|
||||
import { formatFileSize, formatMimeType, getFileKind, isImageFile, isTextFile, isPdfFile, isMarkdownFile } from './fileUtils.js'
|
||||
import { t } from './i18n'
|
||||
|
||||
@@ -13,7 +14,8 @@
|
||||
|
||||
const kind = getFileKind(item)
|
||||
$: showImage = isImageFile(item) && content && content.startsWith('data:')
|
||||
$: showText = isTextFile(item) || isMarkdownFile(item)
|
||||
$: showMarkdown = isMarkdownFile(item) && content
|
||||
$: showText = (isTextFile(item) || isMarkdownFile(item)) && content && !showMarkdown
|
||||
$: showPdf = isPdfFile(item)
|
||||
|
||||
function handleKeydown(e) {
|
||||
@@ -44,13 +46,6 @@
|
||||
</div>
|
||||
<div class="preview-meta">{formatFileSize(item.size)} · {formatMimeType(item.mime)}</div>
|
||||
<div class="preview-actions">
|
||||
<button class="action-btn" on:click={handleOpenExternal} title={t('file.openExternal')} aria-label={t('file.openExternal')}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn action-btn-close" on:click={() => dispatch('close')} title="Close" aria-label="Close preview">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
@@ -71,7 +66,11 @@
|
||||
<div class="preview-image-container">
|
||||
<img src={content} alt={item.name} class="preview-image"/>
|
||||
</div>
|
||||
{:else if showText && content}
|
||||
{:else if showMarkdown}
|
||||
<div class="preview-markdown-container">
|
||||
<MarkdownPreview {content} />
|
||||
</div>
|
||||
{:else if showText}
|
||||
<pre class="preview-text"><code>{content}</code></pre>
|
||||
{:else if showPdf}
|
||||
{#if content && content.startsWith('data:')}
|
||||
@@ -91,6 +90,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<footer class="preview-footer">
|
||||
<button class="btn btn-sm" on:click={handleOpenExternal}>{t('file.openExternal')}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -260,4 +262,20 @@
|
||||
.btn-sm:hover {
|
||||
background: #222233;
|
||||
}
|
||||
|
||||
.preview-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-markdown-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
export let selectedNode = null
|
||||
export let wailsCall = async () => {}
|
||||
|
||||
// ===== Debug helper =====
|
||||
function _fdl(msg) {
|
||||
try { wailsCall('WriteDebugLog', '[FilesTab] ' + msg) } catch(e) {}
|
||||
}
|
||||
|
||||
// ===== Events =====
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -54,6 +59,14 @@
|
||||
loadFolder(nodeId)
|
||||
}
|
||||
|
||||
// ===== React to selectedNode changes =====
|
||||
let lastLoadedNodeId = null
|
||||
$: if (selectedNode && selectedNode.id && selectedNode.id !== lastLoadedNodeId) {
|
||||
_fdl('selectedNode changed: id=' + selectedNode.id + ' title=' + selectedNode.title + ' type=' + selectedNode.type)
|
||||
lastLoadedNodeId = selectedNode.id
|
||||
resetToNode(selectedNode.id)
|
||||
}
|
||||
|
||||
export function addFile() {
|
||||
_addFile()
|
||||
}
|
||||
@@ -79,12 +92,14 @@
|
||||
loadingFiles = true
|
||||
try {
|
||||
let items = await wailsCall('ListItems', folderId) || []
|
||||
_fdl('loadFolder nodeId=' + folderId + ' count=' + items.length + ' first=' + (items[0] ? JSON.stringify({id: items[0].id, name: items[0].name, type: items[0].type, fileId: items[0].fileId}) : 'none'))
|
||||
items.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
|
||||
return (a.name || '').localeCompare(b.name || '')
|
||||
})
|
||||
fileItems = items
|
||||
} catch (e) {
|
||||
_fdl('loadFolder ERROR nodeId=' + folderId + ' err=' + String(e))
|
||||
fileItems = []
|
||||
}
|
||||
loadingFiles = false
|
||||
@@ -126,10 +141,13 @@
|
||||
|
||||
// ===== File preview =====
|
||||
async function _openPreview(item) {
|
||||
_fdl('openPreview item=' + JSON.stringify({id: item.id, name: item.name, type: item.type, fileId: item.fileId}))
|
||||
// For .md files: check if linked to a note, open note editor instead
|
||||
if (item && item.fileId && isMarkdownFile(item)) {
|
||||
_fdl('isMarkdown=true fileId=' + item.fileId)
|
||||
try {
|
||||
const action = await wailsCall('CheckFileAction', item.fileId)
|
||||
_fdl('CheckFileAction result=' + JSON.stringify(action))
|
||||
if (action.action === 'note') {
|
||||
dispatch('openNote', { id: action.noteId, title: action.noteTitle })
|
||||
return
|
||||
@@ -138,9 +156,13 @@
|
||||
await wailsCall('OpenFile', item.fileId)
|
||||
return
|
||||
}
|
||||
// action === 'preview' → fall through to built-in preview below
|
||||
} catch (e) {
|
||||
_fdl('CheckFileAction ERROR: ' + String(e))
|
||||
console.warn('CheckFileAction failed, falling back to preview:', e)
|
||||
}
|
||||
} else {
|
||||
_fdl('isMarkdown=false or no fileId, type=' + item.type)
|
||||
}
|
||||
|
||||
previewItem = item
|
||||
@@ -152,6 +174,10 @@
|
||||
previewContent = await wailsCall('GetFileBase64', item.fileId) || ''
|
||||
} else if (needsTextPreview(item)) {
|
||||
previewContent = await wailsCall('ReadFileText', item.fileId) || ''
|
||||
} else if (item.fileId && item.type !== 'folder') {
|
||||
// Fallback: try to read as text for unknown types
|
||||
_fdl('fallback text read for item=' + item.name + ' mime=' + item.mime)
|
||||
previewContent = await wailsCall('ReadFileText', item.fileId) || ''
|
||||
}
|
||||
} catch (e) {
|
||||
previewError = String(e)
|
||||
@@ -731,4 +757,14 @@
|
||||
.summary-warn { margin-top: 8px; padding: 8px 12px; background: #3a2a22; border-radius: 6px; color: #ffaa66; font-size: 13px; }
|
||||
|
||||
.rename-error { color: #ff6b6b; font-size: 12px; margin-top: 4px; }
|
||||
|
||||
/* Button styles (mirroring global App.svelte .btn) */
|
||||
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.btn:hover { background: #222233; }
|
||||
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
|
||||
.btn-primary:hover { background: #4f46e5; }
|
||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn-danger { color: #ff6b6b; border-color: #4a2222; }
|
||||
.btn-danger:hover { background: #3a2222; }
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="markdown-body" on:click={handleClick} role="article">
|
||||
<div class="markdown-body" on:click={handleClick} role="article" tabindex="0" on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClick(e) }}>
|
||||
{#if error}
|
||||
<div class="md-error">
|
||||
<p>⚠️ {t('note.preview.error')}</p>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
.note-card-info { flex: 1; min-width: 0; }
|
||||
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
|
||||
.note-card-date { font-size: 11px; color: #555; }
|
||||
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
|
||||
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; position: absolute; top: 8px; right: 8px; }
|
||||
.note-card:hover .note-card-actions { opacity: 1; }
|
||||
.note-action-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: #666; cursor: pointer; transition: background 0.12s, color 0.12s; }
|
||||
.note-action-btn:hover { background: #2a2a3c; color: #ccc; }
|
||||
@@ -107,4 +107,14 @@
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
|
||||
.empty-state p { margin: 0; font-size: 14px; color: #666; }
|
||||
.empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; }
|
||||
|
||||
/* Button styles (mirroring global App.svelte .btn) */
|
||||
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.btn:hover { background: #222233; }
|
||||
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
|
||||
.btn-primary:hover { background: #4f46e5; }
|
||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn-danger { color: #ff6b6b; border-color: #4a2222; }
|
||||
.btn-danger:hover { background: #3a2222; }
|
||||
</style>
|
||||
|
||||
@@ -78,8 +78,8 @@ export function getFileKind(item) {
|
||||
}
|
||||
|
||||
const imageMimes = ['image/jpeg','image/png','image/gif','image/webp','image/bmp','image/tiff','image/avif','image/svg+xml']
|
||||
const textMimes = ['text/plain','text/html','text/css','text/javascript','application/json','application/xml','application/x-yaml','text/x-shellscript']
|
||||
const codeNames = ['txt','log','conf','ini','yaml','yml','json','xml','csv','sh','py','js','ts','css','html','md','markdown','cfg']
|
||||
const textMimes = ['text/plain','text/html','text/css','text/javascript','application/json','application/xml','application/x-yaml','text/x-shellscript','text/csv','text/tab-separated-values','text/x-python','text/x-java','text/x-c','text/x-cpp','text/x-ruby','text/x-perl','text/x-php','text/x-go','text/x-rust','text/markdown']
|
||||
const codeNames = ['txt','log','conf','ini','yaml','yml','json','xml','csv','tsv','sh','py','js','ts','css','html','md','markdown','cfg','env','gitignore','dockerignore','toml','bat','cmd','ps1','sql','graphql','proto','gradle','cmake','makefile','dockerfile','vbs','lua','r','m','scala','kt','swift','dart','elm','erl','ex','exs','fs','fsi','fsx','hs','lhs','ml','mli','pl','pm','rb','rake','rs','scm','ss','clj','cljs','cljc','edn','lisp','lsp','el','vim','vimrc','zsh','bash','fish','csh','ksh','tcsh','awk','sed','make','mk','nim','nims','d','go','java','svelte','vue','jsx','tsx','coffee','litcoffee','scss','sass','less','styl','properties','dotenv','editorconfig','gitattributes','browserslistrc','htaccess','nginx','shader','glsl','vert','frag','asm','h','hpp','cxx','cc','cpp','capnp','flatbuf','prisma','dtd','xsl','xsd','sch','svg','vtt','srt','sub','m3u','m3u8','pls','xspf','cue','toc','nfo','diz','readme','changelog','copying','license','authors','contributors','setup','config','strings','po','pot','locale','translation']
|
||||
|
||||
const imageExts = ['jpg','jpeg','png','gif','webp','bmp','tiff','tif','avif','svg']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user