feat: generic server upload warning

added a generic server upload warning, turning it into a toast w/ options instead of a dialog. fixed some hardcoded translations & tooltips
This commit is contained in:
Maya 2026-03-11 09:58:43 +03:00
parent d18fe38832
commit ba6507b7e6
No known key found for this signature in database
7 changed files with 120 additions and 60 deletions

View File

@ -44,7 +44,7 @@
"partial_support": "This format can only be converted as {direction}.",
"direction_input": "input (from)",
"direction_output": "output (to)",
"video_server_processing": "Video uploads to a server for processing by default, learn how to set it up locally here."
"video_server_processing": "When possible, VERT converts videos locally in your browser. Some conversions may require server-side processing, learn how to set it up locally here."
}
},
"convert": {
@ -62,9 +62,10 @@
"large_file_warning": "Due to browser / device limitations, video to audio conversion is disabled for this file as it is larger than {limit}GB. We recommend using Firefox or Safari for files of this size since they have less limitations.",
"external_warning": {
"title": "External server warning",
"text": "If you choose to convert into a video format, those files will be uploaded to an external server to be converted. Do you want to continue?",
"yes": "Yes",
"no": "No"
"text": "Converting {filename} with its current settings is not supported locally and requires server-side processing. Do you want to continue?",
"yes": "Convert on server",
"no": "Cancel conversion",
"dont_show_again": "Don't show again"
},
"panel": {
"convert_all": "Convert all",
@ -149,6 +150,7 @@
"errors": {
"cant_convert": "We can't convert this file.",
"converter_fallback": {
"title": "Change converter?",
"body": "Conversion failed for {filename}. Would you like to try the next compatible converter {converter}?",
"yes": "Next converter",
"no": "Cancel conversion",

View File

@ -2,7 +2,7 @@
import { m } from "$lib/paraglide/messages";
type Additional = {
fileName: string;
filename: string;
nextConverter: string;
onNext: () => void | Promise<void>;
onCancel: () => void;
@ -10,13 +10,13 @@
let { additional }: { additional: Additional } = $props();
export const title = "An error occurred";
export const title = m["convert.errors.converter_fallback.title"]();
</script>
<div class="flex flex-col gap-4">
<p class="text-black">
{m["convert.errors.converter_fallback.body"]({
filename: additional.fileName,
filename: additional.filename,
converter: additional.nextConverter,
})}
</p>

View File

@ -0,0 +1,47 @@
<script lang="ts" module>
export interface ServerUploadWarningProps {
filename: string;
onProceed: () => void;
onCancel: () => void;
onDontShowAgain: () => void;
}
</script>
<script lang="ts">
import { m } from "$lib/paraglide/messages";
import type { ToastProps } from "$lib/util/toast.svelte";
const toast: ToastProps<ServerUploadWarningProps> = $props();
export const title = m["convert.external_warning.title"]();
</script>
<div class="flex flex-col gap-4">
<p class="text-black">
{m["convert.external_warning.text"]({
filename: toast.additional.filename,
})}
</p>
<div class="flex flex-col gap-2">
<button
onclick={toast.additional.onDontShowAgain}
class="btn rounded-lg h-fit py-2 w-full bg-accent-blue text-black"
>
{m["convert.external_warning.dont_show_again"]()}
</button>
<div class="flex gap-4">
<button
onclick={toast.additional.onProceed}
class="btn rounded-lg h-fit py-2 w-full bg-accent-red-alt text-white"
>
{m["convert.external_warning.yes"]()}
</button>
<button
onclick={toast.additional.onCancel}
class="btn rounded-lg h-fit py-2 w-full"
>
{m["convert.external_warning.no"]()}
</button>
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@
from?: string;
to?: string;
errorMessage?: string;
fileName?: string;
filename?: string;
}
</script>
@ -21,7 +21,7 @@
let submitting = $state(false);
export const title = "An error occurred";
export const title = m["convert.errors.vertd_generic_title"]();
const remove = () => {
ToastManager.remove(toast.id);

View File

@ -112,8 +112,8 @@
<style lang="postcss">
.tooltip {
--border-size: 1px;
@apply fixed bg-panel-alt text-foreground border border-stone-400 dynadark:border-white drop-shadow-lg text-xs rounded-full pointer-events-none z-[999] max-w-xs break-words whitespace-normal;
@apply px-5 py-2.5;
@apply fixed bg-panel-alt text-foreground border border-stone-400 dynadark:border-white drop-shadow-lg text-xs pointer-events-none z-[999] max-w-[200px] break-words whitespace-normal;
@apply px-3.5 py-2 rounded-xl;
}
.tooltip-top {

View File

@ -4,7 +4,6 @@ import { error, log } from "$lib/util/logger";
import { VertFile } from "$lib/types";
import { parseBlob, selectCover } from "music-metadata";
import { writable } from "svelte/store";
import { addDialog } from "./DialogProvider";
import PQueue from "p-queue";
import { getLocale, setLocale } from "$lib/paraglide/runtime";
import { m } from "$lib/paraglide/messages";
@ -236,7 +235,6 @@ class Files {
}
}
private _warningShown = false;
private async _add(file: VertFile | File) {
if (file instanceof VertFile) {
this.files.push(file);
@ -302,43 +300,6 @@ class Files {
},
});
}
// TODO: only show if vertd is needed/requested
const isServerVideo = convName === "vertd";
const acceptedExternalWarning =
localStorage.getItem("acceptedExternalWarning") === "true";
if (isServerVideo && !acceptedExternalWarning && !this._warningShown) {
this._warningShown = true;
const title = m["convert.external_warning.title"]();
const message = m["convert.external_warning.text"]();
const buttons = [
{
text: m["convert.external_warning.no"](),
action: () => {
this.files = [
...this.files.filter(
(f) =>
!f.converters
.map((c) => c.name)
.includes("vertd"),
),
];
this._warningShown = false;
},
},
{
text: m["convert.external_warning.yes"](),
action: () => {
localStorage.setItem(
"acceptedExternalWarning",
"true",
);
this._warningShown = false;
},
},
];
addDialog(title, message, buttons, "warning");
}
}
}
@ -378,7 +339,7 @@ class Files {
if (this.files.length === 0) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dlFiles: any[] = [];
const fileNames: string[] = [];
const filenames: string[] = [];
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
@ -392,7 +353,7 @@ class Files {
let to = result.to;
if (!to.startsWith(".")) to = `.${to}`;
fileNames.push(file.file.name.replace(/\.[^/.]+$/, "") + to);
filenames.push(file.file.name.replace(/\.[^/.]+$/, "") + to);
}
for (let i = 0; i < this.files.length; i++) {
@ -401,19 +362,19 @@ class Files {
if (!result) continue;
let fileName = fileNames[i];
let filename = filenames[i];
// check if this filename appears more than once
const isDuplicate = fileNames.filter((name) => name === fileName).length > 1;
const isDuplicate = filenames.filter((name) => name === filename).length > 1;
if (isDuplicate) {
const nameParts = fileName.lastIndexOf(".");
const nameWithoutExt = fileName.substring(0, nameParts);
const ext = fileName.substring(nameParts);
fileName = `${nameWithoutExt} (${i + 1})${ext}`;
const nameParts = filename.lastIndexOf(".");
const nameWithoutExt = filename.substring(0, nameParts);
const ext = filename.substring(nameParts);
filename = `${nameWithoutExt} (${i + 1})${ext}`;
}
dlFiles.push({
name: fileName,
name: filename,
lastModified: Date.now(),
input: await result.file.arrayBuffer(),
});

View File

@ -5,6 +5,7 @@ import { ToastManager } from "$lib/util/toast.svelte";
import type { Component } from "svelte";
import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte";
import FallbackToast from "$lib/components/functional/popups/FallbackToast.svelte";
import ServerUploadWarning from "$lib/components/functional/popups/ServerUploadWarning.svelte";
import type {
ConversionSettings,
SettingDefinition,
@ -41,6 +42,7 @@ export class VertFile {
private fallbackToastId: number | null = null;
private attemptedConverters = new Set<string>();
private retryingFallback = false;
private vertdWarningToastId: number | null = null;
public isZip = $state(() => this.from === ".zip");
@ -168,6 +170,13 @@ export class VertFile {
}
if (!converter) throw new Error("No converter found");
const canProceed = await this.confirmServerWarning(converter);
if (!canProceed) {
this.cancelled = true;
return;
}
this.attemptedConverters.add(converter.name);
log(["file", "convert"], `using converter: ${converter.name}`);
@ -215,7 +224,7 @@ export class VertFile {
disappearing: false,
message: FallbackToast,
additional: {
fileName: this.file.name,
filename: this.file.name,
nextConverter: nextConverter.name,
onNext: async () => {
if (this.fallbackToastId !== null)
@ -262,6 +271,47 @@ export class VertFile {
return res;
}
private async confirmServerWarning(converter: Converter): Promise<boolean> {
if (converter.name !== "vertd") return true;
if (localStorage.getItem("acceptedExternalWarning") === "true")
return true;
return new Promise((resolve) => {
let resolved = false;
const finish = (shouldProceed: boolean) => {
if (resolved) return;
resolved = true;
if (this.vertdWarningToastId !== null)
ToastManager.remove(this.vertdWarningToastId);
this.vertdWarningToastId = null;
resolve(shouldProceed);
};
if (this.vertdWarningToastId !== null)
ToastManager.remove(this.vertdWarningToastId);
this.vertdWarningToastId = ToastManager.add({
type: "warning",
disappearing: false,
message: ServerUploadWarning,
additional: {
filename: this.file.name,
onProceed: () => {
finish(true);
},
onCancel: () => {
finish(false);
},
onDontShowAgain: () => {
localStorage.setItem("acceptedExternalWarning", "true");
finish(true);
},
},
});
});
}
private async convertZip(converter: Converter): Promise<VertFile> {
const { extractZip, createZip } = await import("$lib/util/zip");
const { default: PQueue } = await import("p-queue");