Skip to content

Commit 9a0148f

Browse files
authored
Merge pull request #85 from xconnio/open-images-window
open images & videos in separate window
2 parents dd51d2c + 44ec032 commit 9a0148f

7 files changed

Lines changed: 241 additions & 244 deletions

File tree

src/components/DesktopSessionHost.vue

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
2-
import { ref, onMounted, onUnmounted, computed, watch, nextTick, type Component } from 'vue'
2+
import { ref, onMounted, onUnmounted, computed, watch, nextTick, markRaw, type Component } from 'vue'
33
import { useRouter } from 'vue-router'
4+
import { type Session } from 'xconn'
45
56
import { useMachinesStore } from '@/stores/machines'
67
import { useSettingsStore } from '@/stores/settings'
@@ -11,9 +12,11 @@ import EmbeddedIndexedFiles from '@/components/EmbeddedIndexedFiles.vue'
1112
import TerminalPanel from '@/components/TerminalPanel.vue'
1213
import ResourceMonitor from '@/components/ResourceMonitor.vue'
1314
import DesktopSettingsPanel from '@/components/DesktopSettingsPanel.vue'
15+
import FilePreviewModal from '@/components/FilePreviewModal.vue'
1416
import FloatingWindow from '@/components/FloatingWindow.vue'
1517
import AppDock from '@/components/AppDock.vue'
1618
import { loadCachedWallpaper, storeWallpaper } from '@/composables/useWallpaperCache'
19+
import { getFilePreviewType, type FilePreviewType } from '@/utils/fileTypes'
1720
1821
const props = defineProps<{ realm: string; active: boolean }>()
1922
@@ -37,6 +40,7 @@ const {
3740
restoreWindow,
3841
toggleMaximize,
3942
updateBounds,
43+
updateTitle,
4044
syncMaximizedBounds,
4145
} = desktopSessionsStore.getOrCreate(props.realm)
4246
@@ -207,6 +211,7 @@ const appComponents: Record<string, Component> = {
207211
documents: EmbeddedIndexedFiles,
208212
'resource-monitor': ResourceMonitor,
209213
settings: DesktopSettingsPanel,
214+
preview: FilePreviewModal,
210215
}
211216
212217
function windowProps(win: { id: string; appId: string; props: Record<string, unknown> }) {
@@ -237,6 +242,13 @@ function windowProps(win: { id: string; appId: string; props: Record<string, unk
237242
realm: props.realm,
238243
focused,
239244
}
245+
case 'preview':
246+
return {
247+
session: win.props.session as Session,
248+
entry: win.props.entry as PreviewEntry,
249+
entries: win.props.entries as PreviewEntry[],
250+
focused,
251+
}
240252
default:
241253
return {
242254
realm: props.realm,
@@ -247,6 +259,30 @@ function windowProps(win: { id: string; appId: string; props: Record<string, unk
247259
}
248260
}
249261
262+
type PreviewEntry = { path: string; name: string; size: number }
263+
264+
const PREVIEW_ICONS: Record<FilePreviewType, string> = {
265+
image: 'bi-image-fill',
266+
video: 'bi-file-earmark-play-fill',
267+
audio: 'bi-file-earmark-music-fill',
268+
pdf: 'bi-file-earmark-pdf-fill',
269+
text: 'bi-file-earmark-text-fill',
270+
none: 'bi-file-earmark-fill',
271+
}
272+
273+
function onPreviewFile(session: Session, entry: PreviewEntry, entries: PreviewEntry[]) {
274+
openWindow({
275+
appId: 'preview',
276+
title: entry.name,
277+
icon: PREVIEW_ICONS[getFilePreviewType(entry.name)],
278+
iconColor: '#334155',
279+
iconBg: '#e2e8f0',
280+
width: 760,
281+
height: 560,
282+
props: { session: markRaw(session), entry, entries },
283+
}, maximizedContainerSize())
284+
}
285+
250286
function onOpenFiles(path: string) {
251287
const filesApp = apps.find((a) => a.id === 'files')!
252288
launchApp(filesApp, path)
@@ -382,6 +418,8 @@ onUnmounted(() => {
382418
v-bind="windowProps(win)"
383419
@close="closeWindow(win.id)"
384420
@open-files="onOpenFiles"
421+
@preview-file="onPreviewFile"
422+
@update-title="updateTitle(win.id, $event)"
385423
/>
386424
</FloatingWindow>
387425
</div>

src/components/EmbeddedDesktopFiles.vue

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { useSessionCacheStore } from '@/stores/sessionCache'
66
import { useSettingsStore } from '@/stores/settings'
77
import { useEntryNavigation } from '@/composables/useEntryNavigation'
88
import { floatingWindowToolbarKey } from '@/composables/floatingWindowToolbar'
9-
import FilePreviewModal from '@/components/FilePreviewModal.vue'
109
import type { FileBrowseResult, FileEntry } from '@/types'
1110
import {
1211
createX25519KeyPair,
@@ -37,6 +36,10 @@ const props = defineProps<{
3736
focused?: boolean
3837
}>()
3938
39+
const emit = defineEmits<{
40+
'preview-file': [session: Session, entry: FileEntry, entries: FileEntry[]]
41+
}>()
42+
4043
const sessionCacheStore = useSessionCacheStore()
4144
const settingsStore = useSettingsStore()
4245
@@ -72,7 +75,6 @@ const supportedFileProcedures = ref({
7275
copy: false,
7376
})
7477
75-
const previewEntry = ref<FileEntry | null>(null)
7678
const navHistory = ref<string[]>([])
7779
const navHistoryIndex = ref(-1)
7880
@@ -385,7 +387,6 @@ function resetExplorerState() {
385387
}
386388
clipboard.value = null
387389
closeActionSheet()
388-
previewEntry.value = null
389390
searchGeneration++
390391
fileSearchActive.value = false
391392
fileSearchQuery.value = ''
@@ -690,12 +691,14 @@ function closeActionSheet() {
690691
operationError.value = ''
691692
}
692693
693-
// Opens a file in the shared FilePreviewModal (handles type detection, streaming
694-
// audio/video over WebRTC, and buffered preview for images/pdf/text internally).
694+
// Opens a file in its own preview window (see FilePreviewModal.vue — handles type
695+
// detection, streaming audio/video over WebRTC, and buffered preview for
696+
// images/pdf/text internally).
695697
function openPreview(entry: FileEntry) {
696698
if (entry.is_dir) return
697699
selectEntry(entry)
698-
previewEntry.value = entry
700+
if (!session.value) return
701+
emit('preview-file', session.value, entry, visibleEntries.value)
699702
}
700703
701704
// Low-level stream: calls onChunk for each decrypted data chunk as it arrives.
@@ -1480,7 +1483,7 @@ function handleGlobalKeydown(e: KeyboardEvent) {
14801483
const target = e.target as HTMLElement
14811484
14821485
if (e.key === 'Escape') {
1483-
if (document.fullscreenElement) return // FilePreviewModal handles its own fullscreen exit
1486+
if (document.fullscreenElement) return // let the browser exit fullscreen first
14841487
if (fileSearchActive.value) { exitFileSearch(); return }
14851488
if (searchMode.value) { exitSearchMode(); return }
14861489
if (propertiesModalVisible.value) { closePropertiesModal(); return }
@@ -1529,10 +1532,6 @@ watch(
15291532
},
15301533
)
15311534
1532-
watch(previewEntry, (entry) => {
1533-
document.body.style.overflow = entry ? 'hidden' : ''
1534-
})
1535-
15361535
function applyThumbnails(entries: FileEntry[]) {
15371536
for (const entry of entries) {
15381537
if (entry.thumbnail && !thumbnailUrls[entry.path]) {
@@ -1562,7 +1561,6 @@ onUnmounted(() => {
15621561
window.removeEventListener('resize', updateViewMode)
15631562
document.removeEventListener('keydown', handleGlobalKeydown)
15641563
disconnectDesktopSession()
1565-
previewEntry.value = null
15661564
for (const url of Object.values(thumbnailUrls)) URL.revokeObjectURL(url)
15671565
})
15681566
@@ -2038,13 +2036,6 @@ onUnmounted(() => {
20382036
</div>
20392037
</div>
20402038

2041-
<FilePreviewModal
2042-
v-if="previewEntry && session"
2043-
:session="session"
2044-
:entry="previewEntry"
2045-
@close="previewEntry = null"
2046-
/>
2047-
20482039
<Transition name="dl-toast">
20492040
<div v-if="downloadProgress" class="dl-toast">
20502041
<div class="dl-toast-header">

src/components/EmbeddedIndexedFiles.vue

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { type Session } from 'xconn'
44
55
import { useSessionCacheStore } from '@/stores/sessionCache'
66
import { useSettingsStore } from '@/stores/settings'
7-
import FilePreviewModal from '@/components/FilePreviewModal.vue'
87
import { useEntryNavigation } from '@/composables/useEntryNavigation'
98
import {
109
createX25519KeyPair,
@@ -47,6 +46,7 @@ const props = defineProps<{
4746
4847
const emit = defineEmits<{
4948
'open-files': [path: string]
49+
'preview-file': [session: Session, entry: IndexEntry, entries: IndexEntry[]]
5050
}>()
5151
5252
const sessionCacheStore = useSessionCacheStore()
@@ -70,7 +70,6 @@ const activeSession = shallowRef<Session | null>(null)
7070
let activeKeys: EncryptionKeys | null = null
7171
let activeCategories: string[] = []
7272
73-
const previewEntry = ref<IndexEntry | null>(null)
7473
const previewSession = shallowRef<Session | null>(null)
7574
7675
const contextMenu = ref<{ x: number; y: number; entry: IndexEntry } | null>(null)
@@ -94,12 +93,6 @@ function handleContextMenuKeydown(e: KeyboardEvent) {
9493
if (e.key === 'Escape') closeContextMenu()
9594
}
9695
97-
const previewModalProps = computed(() =>
98-
previewEntry.value && previewSession.value
99-
? { session: previewSession.value, entry: previewEntry.value }
100-
: null
101-
)
102-
10396
function updateViewMode() { isGridView.value = window.innerWidth >= 768 }
10497
10598
const viewConfig = computed(() => {
@@ -143,7 +136,7 @@ const displayEntries = computed(() =>
143136
144137
function openEntry(entry: IndexEntry) {
145138
if (!previewSession.value) return
146-
previewEntry.value = entry
139+
emit('preview-file', previewSession.value, entry, displayEntries.value)
147140
}
148141
149142
const { handleNavKey } = useEntryNavigation({
@@ -156,7 +149,6 @@ const { handleNavKey } = useEntryNavigation({
156149
157150
function handleKeydown(e: KeyboardEvent) {
158151
if (!props.focused) return
159-
if (previewEntry.value) return
160152
const target = e.target as HTMLElement
161153
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return
162154
handleNavKey(e)
@@ -359,13 +351,6 @@ onUnmounted(() => {
359351
</div>
360352
</Teleport>
361353

362-
<FilePreviewModal
363-
v-if="previewModalProps"
364-
:session="previewModalProps.session"
365-
:entry="previewModalProps.entry"
366-
@close="previewEntry = null"
367-
/>
368-
369354
<div v-if="isConnecting || isLoading" class="state-center">
370355
<div class="pulse-ring" :style="{ background: viewConfig.bg }">
371356
<i class="bi" :class="viewConfig.icon" :style="{ color: viewConfig.color }"></i>

0 commit comments

Comments
 (0)