Skip to content

Commit ed70dcb

Browse files
committed
add crop & rotate editing to image viewer
1 parent ea750d0 commit ed70dcb

6 files changed

Lines changed: 662 additions & 94 deletions

File tree

src/components/DesktopSessionHost.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ function windowProps(win: { id: string; appId: string; props: Record<string, unk
279279
case 'video-player':
280280
return {
281281
session: win.props.session as Session,
282+
realm: props.realm,
282283
entry: win.props.entry as PreviewEntry,
283284
entries: win.props.entries as PreviewEntry[],
284285
focused,

src/components/EmbeddedDesktopFiles.vue

Lines changed: 17 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script setup lang="ts">
22
import { computed, inject, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
3-
import { ApplicationError, Progress, type Result, type Session } from 'xconn'
3+
import { ApplicationError, type Session } from 'xconn'
44
55
import { useSessionCacheStore } from '@/stores/sessionCache'
6+
import { useSessionEncryptionStore } from '@/stores/sessionEncryption'
67
import { useSettingsStore } from '@/stores/settings'
78
import { useEntryNavigation } from '@/composables/useEntryNavigation'
89
import { floatingWindowToolbarKey } from '@/composables/floatingWindowToolbar'
@@ -14,18 +15,16 @@ import {
1415
decryptPayload,
1516
type EncryptionKeys,
1617
} from '@/utils/encryption'
18+
import { uploadFileToPath } from '@/utils/fileUpload'
1719
import { formatSize, getFilePreviewType, isFirefoxBrowser } from '@/utils/fileTypes'
1820
import { formatDesktopError, isNoSuchProcedureException } from '@/utils/desktopError'
1921
20-
const procedureKeyExchange = 'io.xconn.deskconn.deskconnd.key.exchange'
2122
const procedureFileBrowse = 'io.xconn.deskconn.deskconnd.file.browse'
2223
const procedureFileRename = 'io.xconn.deskconn.deskconnd.file.rename'
2324
const procedureFileDelete = 'io.xconn.deskconn.deskconnd.file.delete'
2425
const procedureFileCopy = 'io.xconn.deskconn.deskconnd.file.copy'
2526
const procedureFileDownload = 'io.xconn.deskconn.deskconnd.file.download'
2627
const procedureFileSearch = 'io.xconn.deskconn.deskconnd.file.search'
27-
const procedureFileUpload = 'io.xconn.deskconn.deskconnd.file.upload'
28-
const UPLOAD_CHUNK_SIZE = 1024 * 1024
2928
const outsideHomeMessage = 'Access denied. You can only browse files inside the home directory.'
3029
3130
const props = defineProps<{
@@ -41,6 +40,7 @@ const emit = defineEmits<{
4140
}>()
4241
4342
const sessionCacheStore = useSessionCacheStore()
43+
const sessionEncryptionStore = useSessionEncryptionStore()
4444
const settingsStore = useSettingsStore()
4545
4646
// Absent when there's no FloatingWindow ancestor (e.g. the standalone /files
@@ -398,25 +398,18 @@ function resetExplorerState() {
398398
async function performKeyExchange(): Promise<boolean> {
399399
if (!session.value) return false
400400
401-
const { publicKey, privateKey } = createX25519KeyPair()
402-
403-
let result
401+
// Shared across every window on this realm's session (see
402+
// sessionEncryption.ts) — the backend keeps only one key per session, so
403+
// Files/Pictures/an Image Viewer save all have to reuse the same one.
404404
try {
405-
result = await session.value.call(procedureKeyExchange, [publicKey])
405+
encryptionKeys.value = await sessionEncryptionStore.getOrExchange(session.value, props.realm)
406+
return true
406407
} catch (error) {
407408
if (isNoSuchProcedureException(error)) {
408409
return false
409410
}
410411
throw error
411412
}
412-
413-
const serverKeyBytes = result.args?.[0] as Uint8Array
414-
if (!serverKeyBytes?.length) {
415-
throw new Error('Invalid server public key received during key exchange')
416-
}
417-
418-
encryptionKeys.value = await deriveSessionKeys(privateKey, serverKeyBytes)
419-
return true
420413
}
421414
422415
async function probeFileOperation(procedure: string): Promise<boolean> {
@@ -1058,7 +1051,6 @@ async function uploadFileToRemote(file: File) {
10581051
if (!session.value || !currentBrowse.value) return
10591052
10601053
const destPath = currentBrowse.value.path
1061-
const startTime = Date.now()
10621054
const controller = new AbortController()
10631055
10641056
uploadProgress.value = {
@@ -1069,79 +1061,19 @@ async function uploadFileToRemote(file: File) {
10691061
cancel: () => controller.abort(),
10701062
}
10711063
1072-
let sendKey: Uint8Array | null = encryptionKeys.value?.encryptKey ?? null
1073-
const needsKeyExchange = sendKey === null
1074-
const { publicKey, privateKey } = createX25519KeyPair()
1075-
1076-
let seq = 0
1077-
let fileOffset = 0
1078-
let ackResolve: ((r: Result) => void) | null = null
1079-
1080-
const progressHandler = async (result: Result) => {
1081-
ackResolve?.(result)
1082-
ackResolve = null
1083-
}
1084-
1085-
function waitForAck(): Promise<Result> {
1086-
return new Promise((resolve, reject) => {
1087-
ackResolve = resolve
1088-
controller.signal.addEventListener('abort', () => reject(new Error('cancelled')), { once: true })
1089-
})
1090-
}
1091-
1092-
function enc(obj: object): Uint8Array {
1093-
return encryptPayload(new TextEncoder().encode(JSON.stringify(obj)), sendKey!)
1094-
}
1095-
1096-
async function* frames(): AsyncGenerator<Progress> {
1097-
if (needsKeyExchange) {
1098-
yield new Progress(['K', publicKey], {}, { progress: true })
1099-
const ack = await waitForAck()
1100-
const raw = ack.args?.[0] as Uint8Array
1101-
if (!raw?.length) throw new Error('Empty key exchange response from server')
1102-
const keys = await deriveSessionKeys(privateKey, raw.slice(4))
1103-
sendKey = keys.encryptKey
1104-
}
1105-
1106-
yield new Progress(['I', seq++, enc({ remote_path: destPath, source_is_dir: false, target_is_dir_hint: true })], {}, { progress: true })
1107-
await waitForAck()
1108-
1109-
yield new Progress(['H', seq++, enc({ name: file.name, rel_path: file.name, size: file.size, mode: 0, is_dir: false })], {}, { progress: true })
1110-
1111-
while (fileOffset < file.size) {
1112-
await waitForAck()
1113-
const chunkBytes = new Uint8Array(await file.slice(fileOffset, fileOffset + UPLOAD_CHUNK_SIZE).arrayBuffer())
1114-
fileOffset += chunkBytes.length
1115-
if (uploadProgress.value) {
1116-
uploadProgress.value.sent = fileOffset
1117-
const elapsed = (Date.now() - startTime) / 1000
1118-
uploadProgress.value.speed = elapsed > 0 ? fileOffset / elapsed : 0
1119-
}
1120-
yield new Progress(['D', seq++, encryptPayload(chunkBytes, sendKey!)], {}, { progress: true })
1121-
}
1122-
1123-
await waitForAck()
1124-
yield new Progress(['E', seq], {}, {})
1125-
}
1126-
1127-
const gen = frames()
1128-
const progressFunc = async () => (await gen.next()).value as Progress
1129-
11301064
try {
1131-
await session.value.callProgressiveProgress(procedureFileUpload, progressFunc, progressHandler)
1065+
await uploadFileToPath(session.value, props.realm, destPath, file.name, file, (p) => {
1066+
if (!uploadProgress.value) return
1067+
uploadProgress.value.sent = p.sent
1068+
uploadProgress.value.total = p.total
1069+
uploadProgress.value.speed = p.speed
1070+
}, controller.signal)
11321071
await refreshCurrentPath()
11331072
} catch (err) {
1073+
// On abort, uploadFileToPath already tells the backend to discard its
1074+
// per-session upload state before rethrowing — nothing left to surface.
11341075
if (!controller.signal.aborted) {
11351076
operationError.value = err instanceof Error ? err.message : 'Upload failed'
1136-
} else {
1137-
// Aborting mid-stream never tells the backend the call ended, so it keeps
1138-
// its per-session upload state around — the next upload would then get
1139-
// stuck waiting on a sequence number that will never arrive. A plain
1140-
// (non-progressive) call with any message type other than 'E' makes the
1141-
// backend discard that stale state.
1142-
try {
1143-
await session.value?.call(procedureFileUpload, ['C'])
1144-
} catch { /* best-effort cleanup, ignore failures */ }
11451077
}
11461078
} finally {
11471079
uploadProgress.value = null

src/components/EmbeddedIndexedFiles.vue

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@ import { ref, shallowRef, computed, onMounted, onUnmounted, nextTick } from 'vue
33
import { type Session } from 'xconn'
44
55
import { useSessionCacheStore } from '@/stores/sessionCache'
6+
import { useSessionEncryptionStore } from '@/stores/sessionEncryption'
67
import { useSettingsStore } from '@/stores/settings'
78
import { useEntryNavigation } from '@/composables/useEntryNavigation'
89
import {
9-
createX25519KeyPair,
10-
deriveSessionKeys,
1110
encryptPayload,
1211
decryptPayload,
1312
type EncryptionKeys,
1413
} from '@/utils/encryption'
1514
import { DESKTOP_OFFLINE_MESSAGE, formatDesktopError } from '@/utils/desktopError'
1615
17-
const procedureKeyExchange = 'io.xconn.deskconn.deskconnd.key.exchange'
1816
const procedureIndexQuery = 'io.xconn.deskconn.deskconnd.index.query'
1917
2018
interface IndexEntry {
@@ -50,6 +48,7 @@ const emit = defineEmits<{
5048
}>()
5149
5250
const sessionCacheStore = useSessionCacheStore()
51+
const sessionEncryptionStore = useSessionEncryptionStore()
5352
const settingsStore = useSettingsStore()
5453
5554
const isConnecting = ref(true)
@@ -294,11 +293,10 @@ async function load() {
294293
isLoading.value = true
295294
296295
try {
297-
const { publicKey, privateKey } = createX25519KeyPair()
298-
const keyResult = await sess.call(procedureKeyExchange, [publicKey])
299-
const serverPublicKey = keyResult.args?.[0] as Uint8Array
300-
if (!serverPublicKey?.length) throw new Error('Invalid key exchange response')
301-
const keys = await deriveSessionKeys(privateKey, serverPublicKey)
296+
// Shared across every window on this realm's session (see
297+
// sessionEncryption.ts) — the backend keeps only one key per session, so
298+
// Files/Pictures/an Image Viewer save all have to reuse the same one.
299+
const keys = await sessionEncryptionStore.getOrExchange(sess, props.realm)
302300
303301
const backendCat = viewConfig.value.backendCategory
304302
const categories = backendCat !== null ? [backendCat] : ['pdfs', 'texts', 'documents']

0 commit comments

Comments
 (0)