Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src-tauri/src/commands/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ fn read_dir_recursive(dir_path: &Path, max_depth: u32) -> Result<Vec<FileNode>,
} else if metadata.is_file() {
let ext = path.extension().map(|e| e.to_string_lossy().to_lowercase());

// Include markdown files and common text files
// Include markdown files, common text files, and images
let include = matches!(
ext.as_deref(),
Some("md")
Expand All @@ -178,6 +178,16 @@ fn read_dir_recursive(dir_path: &Path, max_depth: u32) -> Result<Vec<FileNode>,
| Some("ts")
| Some("xml")
| Some("csv")
| Some("png")
| Some("jpg")
| Some("jpeg")
| Some("gif")
| Some("bmp")
| Some("svg")
| Some("webp")
| Some("ico")
| Some("tiff")
| Some("tif")
);

if include {
Expand Down
4 changes: 3 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<Sidebar />
<main class="editor-area">
<div v-if="tabsStore.activeTab" class="editor-content">
<Editor v-if="editorModeStore.mode === 'wysiwyg'" ref="editorRef" />
<ImageViewer v-if="tabsStore.activeTab.isImage" />
<Editor v-else-if="editorModeStore.mode === 'wysiwyg'" ref="editorRef" />
<SourceEditor v-else />
</div>
<div v-else class="no-tab-placeholder">
Expand Down Expand Up @@ -45,6 +46,7 @@ import { invoke } from '@tauri-apps/api/core'
import TabBar from './components/tabs/TabBar.vue'
import Editor from './components/Editor.vue'
import SourceEditor from './components/source/SourceEditor.vue'
import ImageViewer from './components/ImageViewer.vue'
import Sidebar from './components/sidebar/Sidebar.vue'
import OutlinePanel from './components/sidebar/OutlinePanel.vue'
import StatusBar from './components/StatusBar.vue'
Expand Down
99 changes: 99 additions & 0 deletions src/components/ImageViewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<div class="image-viewer">
<div class="image-viewer-container">
<img
v-if="assetUrl"
:src="assetUrl"
:alt="fileName"
class="image-preview"
@error="handleError"
/>
<div v-if="loadError" class="image-error">
<p>Failed to load image</p>
<p class="image-error-path">{{ filePath }}</p>
</div>
</div>
<div class="image-info-bar">
<span class="image-file-name">{{ fileName }}</span>
</div>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/core'
import { useTabsStore } from '../stores/tabs'

const tabsStore = useTabsStore()
const loadError = ref(false)

const filePath = computed(() => tabsStore.activeTab?.filePath ?? '')

const fileName = computed(() => {
if (!filePath.value) return ''
const parts = filePath.value.split('/')
return parts[parts.length - 1] || ''
})

const assetUrl = computed(() => {
if (!filePath.value) return ''
return convertFileSrc(filePath.value)
})

function handleError() {
loadError.value = true
}
</script>

<style>
.image-viewer {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--editor-bg, #fff);
}

.image-viewer-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding: 24px;
}

.image-preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.image-info-bar {
display: flex;
align-items: center;
padding: 6px 16px;
border-top: 1px solid var(--sidebar-border, #e0e0e0);
background: var(--sidebar-bg, #f5f5f5);
font-size: 12px;
color: var(--text-secondary, #888);
}

.image-file-name {
font-family: 'SF Mono', Menlo, Consolas, monospace;
}

.image-error {
text-align: center;
color: var(--text-secondary, #888);
}

.image-error-path {
font-family: 'SF Mono', Menlo, Consolas, monospace;
font-size: 12px;
color: var(--text-secondary, #aaa);
margin-top: 8px;
}
</style>
27 changes: 27 additions & 0 deletions src/components/sidebar/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,40 @@ const sidebar = useSidebarStore()
const tabs = useTabsStore()
const recentFiles = useRecentFilesStore()

/** Image file extensions that should open with the system viewer instead of as a tab */
const IMAGE_EXTENSIONS = new Set([
'png',
'jpg',
'jpeg',
'gif',
'bmp',
'svg',
'webp',
'ico',
'tiff',
'tif',
])

/** Check if a file path is an image based on its extension */
function isImageFile(path: string): boolean {
const ext = path.split('.').pop()?.toLowerCase() ?? ''
return IMAGE_EXTENSIONS.has(ext)
}

/** The currently selected file path (from the active tab) */
const selectedFilePath = computed(() => {
return tabs.activeTab?.filePath ?? null
})

/** Handle file selection in the tree — reads file from disk and opens in tab */
async function handleFileSelect(path: string) {
// Image files open in the built-in image viewer tab
if (isImageFile(path)) {
tabs.openImageFile(path)
recentFiles.addRecentFile(path)
return
}

// openFile handles deduplication: switches to existing tab or reads from disk
const tab = await tabs.openFile(path)
if (tab) {
Expand Down
28 changes: 28 additions & 0 deletions src/stores/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const useTabsStore = defineStore('tabs', () => {
filePath,
isModified: false,
isUntitled,
isImage: false,
editorState: createDefaultEditorState(content),
}

Expand Down Expand Up @@ -268,6 +269,32 @@ export const useTabsStore = defineStore('tabs', () => {
}
}

/**
* Open an image file in a preview tab (no text content loaded).
*/
function openImageFile(filePath: string): Tab {
// Check for existing tab
const existing = tabs.value.find((t) => t.filePath === filePath)
if (existing) {
activeTabId.value = existing.id
return existing
}

const tab: Tab = {
id: generateId(),
title: fileNameFromPath(filePath),
filePath,
isModified: false,
isUntitled: false,
isImage: true,
editorState: createDefaultEditorState(),
}

tabs.value.push(tab)
activeTabId.value = tab.id
return tab
}

/**
* Show a native file picker dialog and open the selected file.
*/
Expand Down Expand Up @@ -305,6 +332,7 @@ export const useTabsStore = defineStore('tabs', () => {
nextTab,
previousTab,
openFile,
openImageFile,
openFileDialog,
}
})
2 changes: 2 additions & 0 deletions src/types/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface Tab {
isModified: boolean
/** Whether the tab has never been saved */
isUntitled: boolean
/** Whether this tab displays an image file (not a text editor) */
isImage: boolean
/** Persisted editor state for this tab */
editorState: EditorState
}
Expand Down
Loading