Skip to content

Commit 79ed53b

Browse files
add
1 parent 01233d9 commit 79ed53b

6 files changed

Lines changed: 142 additions & 134 deletions

File tree

src/frontend/src/lib/components/FileViewerOverlay.svelte

Lines changed: 134 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@
5151
5252
type MediaKind = 'image' | 'video' | 'audio' | 'other';
5353
54+
type ImageInfo = {
55+
title: string;
56+
message?: string | null;
57+
mime?: string | null;
58+
};
59+
60+
type IconComponent = typeof Link;
61+
62+
type ToolbarAction = {
63+
key: string;
64+
label: string;
65+
activeLabel?: string;
66+
icon: IconComponent;
67+
activeIcon?: IconComponent;
68+
onClick: () => void;
69+
disabled?: boolean;
70+
active?: boolean;
71+
isVisible: boolean;
72+
};
73+
5474
const heicExtensions = ['.heic', '.heif'];
5575
5676
let sniffedKind = $state<MediaKind | null>(null);
@@ -112,30 +132,32 @@
112132
heicConverting = true;
113133
heicError = null;
114134
115-
heicConvertPromise = (async () => {
116-
try {
117-
const result = await heicTo({ blob: sourceBlob, type: 'image/png' });
118-
const pngBlob = result instanceof Blob ? result : null;
119-
if (!pngBlob || token !== heicConversionToken) return null;
120-
heicConvertedBlob = pngBlob;
121-
heicConvertedUrl = URL.createObjectURL(pngBlob);
122-
return pngBlob;
123-
} catch {
124-
if (token === heicConversionToken) {
125-
heicError = 'Could not convert this HEIC image.';
126-
}
127-
return null;
128-
} finally {
129-
if (token === heicConversionToken) {
130-
heicConverting = false;
131-
heicConvertPromise = null;
132-
}
133-
}
134-
})();
135+
heicConvertPromise = runHeicConversion(token);
135136
136137
return await heicConvertPromise;
137138
}
138139
140+
async function runHeicConversion(token: number) {
141+
try {
142+
const result = await heicTo({ blob: sourceBlob!, type: 'image/png' });
143+
const pngBlob = result instanceof Blob ? result : null;
144+
if (!pngBlob || token !== heicConversionToken) return null;
145+
heicConvertedBlob = pngBlob;
146+
heicConvertedUrl = URL.createObjectURL(pngBlob);
147+
return pngBlob;
148+
} catch {
149+
if (token === heicConversionToken) {
150+
heicError = 'Could not convert this HEIC image.';
151+
}
152+
return null;
153+
} finally {
154+
if (token === heicConversionToken) {
155+
heicConverting = false;
156+
heicConvertPromise = null;
157+
}
158+
}
159+
}
160+
139161
function handleDownloadOriginal() {
140162
if (ondownload) {
141163
ondownload();
@@ -214,9 +236,72 @@
214236
const isImageUnsupported = $derived(!isHeic && imageSupport?.status === 'unsupported');
215237
const imageSupportMessage = $derived(imageSupport?.message ?? null);
216238
239+
const imageInfo = $derived<ImageInfo | null>(
240+
isHeic && !heicConverting && !heicConvertedUrl
241+
? {
242+
title: heicError ?? 'Unable to preview this HEIC image.',
243+
message: imageSupportMessage,
244+
mime: sniffedMime
245+
}
246+
: isImageUnsupported
247+
? {
248+
title: 'This image format is not supported in this browser.',
249+
message: imageSupportMessage,
250+
mime: sniffedMime
251+
}
252+
: null
253+
);
254+
255+
const toolbarActions = $derived<ToolbarAction[]>([
256+
{
257+
key: 'copy-text',
258+
isVisible: contentText !== null,
259+
label: 'Copy Text',
260+
activeLabel: 'Copied Text',
261+
icon: Copy,
262+
activeIcon: Check,
263+
active: textCopied,
264+
onClick: handleCopyText
265+
},
266+
{
267+
key: 'copy-link',
268+
isVisible: Boolean(oncopylink),
269+
label: 'Copy Link',
270+
activeLabel: 'Copied Link',
271+
icon: Link,
272+
activeIcon: Check,
273+
active: copied,
274+
onClick: handleCopyLink
275+
},
276+
{
277+
key: 'save-original',
278+
isVisible: Boolean(ondownload) && isHeic,
279+
label: 'Save Original',
280+
icon: Download,
281+
onClick: handleDownloadOriginal
282+
},
283+
{
284+
key: 'save-png',
285+
isVisible: Boolean(ondownload) && isHeic,
286+
label: heicConverting && !heicConvertedBlob ? 'Converting...' : 'Save PNG',
287+
icon: Download,
288+
onClick: handleDownloadPng,
289+
disabled: heicConverting && !heicConvertedBlob
290+
},
291+
{
292+
key: 'save',
293+
isVisible: Boolean(ondownload) && !isHeic,
294+
label: 'Save',
295+
icon: Download,
296+
onClick: () => ondownload?.()
297+
}
298+
]);
299+
300+
const visibleToolbarActions = $derived(toolbarActions.filter((action) => action.isVisible));
301+
217302
$effect(() => {
218303
if (!sourceBlob || !isHeic) return;
219-
void convertHeicToPng();
304+
convertHeicToPng();
220305
});
221306
222307
function handleKeydown(event: KeyboardEvent) {
@@ -253,75 +338,19 @@
253338
<span class="truncate text-sm font-medium text-white">{baseName}</span>
254339
</div>
255340
<div class="flex items-center gap-1">
256-
{#if contentText !== null}
341+
{#each visibleToolbarActions as action (action.key)}
342+
{@const Icon = action.active ? (action.activeIcon ?? action.icon) : action.icon}
257343
<Button
258344
variant="ghost"
259345
size="sm"
260346
class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white"
261-
onclick={handleCopyText}
347+
disabled={action.disabled}
348+
onclick={action.onClick}
262349
>
263-
{#if textCopied}
264-
<Check class="h-3.5 w-3.5" />
265-
Copied Text
266-
{:else}
267-
<Copy class="h-3.5 w-3.5" />
268-
Copy Text
269-
{/if}
350+
<Icon class="h-3.5 w-3.5" />
351+
{action.active ? (action.activeLabel ?? action.label) : action.label}
270352
</Button>
271-
{/if}
272-
{#if oncopylink}
273-
<Button
274-
variant="ghost"
275-
size="sm"
276-
class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white"
277-
onclick={handleCopyLink}
278-
>
279-
{#if copied}
280-
<Check class="h-3.5 w-3.5" />
281-
Copied Link
282-
{:else}
283-
<Link class="h-3.5 w-3.5" />
284-
Copy Link
285-
{/if}
286-
</Button>
287-
{/if}
288-
{#if ondownload}
289-
{#if isHeic}
290-
<Button
291-
variant="ghost"
292-
size="sm"
293-
class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white"
294-
onclick={handleDownloadOriginal}
295-
>
296-
<Download class="h-3.5 w-3.5" />
297-
Save Original
298-
</Button>
299-
<Button
300-
variant="ghost"
301-
size="sm"
302-
class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white"
303-
disabled={heicConverting && !heicConvertedBlob}
304-
onclick={handleDownloadPng}
305-
>
306-
<Download class="h-3.5 w-3.5" />
307-
{#if heicConverting && !heicConvertedBlob}
308-
Converting...
309-
{:else}
310-
Save PNG
311-
{/if}
312-
</Button>
313-
{:else}
314-
<Button
315-
variant="ghost"
316-
size="sm"
317-
class="h-7 gap-1.5 px-2 text-xs text-white/70 hover:bg-white/10 hover:text-white"
318-
onclick={ondownload}
319-
>
320-
<Download class="h-3.5 w-3.5" />
321-
Save
322-
</Button>
323-
{/if}
324-
{/if}
353+
{/each}
325354
</div>
326355
</div>
327356

@@ -338,51 +367,31 @@
338367
Detecting file type...
339368
</div>
340369
{:else if isImage}
341-
{#if isHeic}
342-
{#if heicConverting && !heicConvertedUrl}
343-
<div class="flex h-full items-center justify-center text-xs text-white/60">
344-
<div class="flex items-center gap-2">
345-
<Spinner class="size-4" />
346-
<span>Converting HEIC to PNG...</span>
347-
</div>
370+
{#if isHeic && heicConverting && !heicConvertedUrl}
371+
<div class="flex h-full items-center justify-center text-xs text-white/60">
372+
<div class="flex items-center gap-2">
373+
<Spinner class="size-4" />
374+
<span>Converting HEIC to PNG...</span>
348375
</div>
349-
{:else if heicConvertedUrl}
350-
<div class="flex h-full items-center justify-center">
351-
<img
352-
src={heicConvertedUrl}
353-
alt={baseName}
354-
title={baseName}
355-
class="max-h-full max-w-full rounded-lg object-contain shadow-2xl"
356-
/>
357-
</div>
358-
{:else}
359-
<div class="flex h-full items-center justify-center">
360-
<div
361-
class="max-w-md rounded-lg border border-white/10 bg-black/60 p-6 text-center"
362-
>
363-
<p class="text-sm font-semibold">
364-
{heicError ?? 'Unable to preview this HEIC image.'}
365-
</p>
366-
{#if imageSupportMessage}
367-
<p class="mt-2 text-xs text-white/60">{imageSupportMessage}</p>
368-
{/if}
369-
{#if sniffedMime}
370-
<p class="mt-2 text-xs text-white/40">Detected: {sniffedMime}</p>
371-
{/if}
372-
</div>
373-
</div>
374-
{/if}
375-
{:else if isImageUnsupported}
376+
</div>
377+
{:else if isHeic && heicConvertedUrl}
378+
<div class="flex h-full items-center justify-center">
379+
<img
380+
src={heicConvertedUrl}
381+
alt={baseName}
382+
title={baseName}
383+
class="max-h-full max-w-full rounded-lg object-contain shadow-2xl"
384+
/>
385+
</div>
386+
{:else if imageInfo}
376387
<div class="flex h-full items-center justify-center">
377388
<div class="max-w-md rounded-lg border border-white/10 bg-black/60 p-6 text-center">
378-
<p class="text-sm font-semibold">
379-
This image format is not supported in this browser.
380-
</p>
381-
{#if imageSupportMessage}
382-
<p class="mt-2 text-xs text-white/60">{imageSupportMessage}</p>
389+
<p class="text-sm font-semibold">{imageInfo.title}</p>
390+
{#if imageInfo.message}
391+
<p class="mt-2 text-xs text-white/60">{imageInfo.message}</p>
383392
{/if}
384-
{#if sniffedMime}
385-
<p class="mt-2 text-xs text-white/40">Detected: {sniffedMime}</p>
393+
{#if imageInfo.mime}
394+
<p class="mt-2 text-xs text-white/40">Detected: {imageInfo.mime}</p>
386395
{/if}
387396
</div>
388397
</div>

src/frontend/src/lib/functions/download.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Api } from '#consts/backend';
2+
import { createDecryptedStream } from '#functions/streams';
23
import { ZipReader } from '@zip.js/zip.js';
3-
import { createDecryptedStream } from './streams';
44

55
export class PasswordRequiredError extends Error {
66
constructor() {

src/frontend/src/lib/functions/libravatar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { hashSHA256 } from './security';
1+
import { hashSHA256 } from '#functions/security';
22

33
export async function make_libravatar_url(email: string) {
44
const hash = await hashSHA256(email);

src/frontend/src/routes/(needs_onboarding)/(navbar_and_footer)/upload/+page.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import { onMount } from 'svelte';
1414
import { CloudOff } from '@lucide/svelte';
1515
16-
import Stage1 from './stage_1.svelte';
17-
import Stage2 from './stage_2.svelte';
18-
import Stage3 from './stage_3.svelte';
16+
const { default: Stage1 } = await import('./stage_1.svelte');
17+
const { default: Stage2 } = await import('./stage_2.svelte');
18+
const { default: Stage3 } = await import('./stage_3.svelte');
1919
import { UploadStage, isWhichUploadStage } from './enums';
2020
2121
const { config: configData } = useConfigQuery();

src/frontend/src/routes/onboarding/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import { OnboardingStep } from './enums';
1111
1212
// Steps
13-
import Step1 from './stage_1.svelte';
14-
import Step2 from './stage_2.svelte';
13+
const { default: Step1 } = await import('./stage_1.svelte');
14+
const { default: Step2 } = await import('./stage_2.svelte');
1515
1616
const { status } = useOnboarding();
1717
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { Config } from '@sveltejs/kit';
21
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
32

43
import node_adapter from '@sveltejs/adapter-node';
@@ -37,4 +36,4 @@ export default {
3736
'#css/*': './src/css/*'
3837
}
3938
}
40-
} satisfies Config;
39+
};

0 commit comments

Comments
 (0)