From 4232c3437bf1292b470ac583fe20582e879dfd2b Mon Sep 17 00:00:00 2001
From: wukko <me@wukko.me>
Date: Sat, 13 Jul 2024 19:15:43 +0600
Subject: [PATCH] web: dialog system & basic small dialog

---
 web/i18n/en/general.json                      |  3 +-
 web/src/components/dialog/DialogHolder.svelte | 28 ++++++++++
 web/src/components/dialog/SmallDialog.svelte  | 55 +++++++++++++++++++
 .../save/buttons/DownloadButton.svelte        | 31 ++++++++++-
 web/src/lib/dialogs.ts                        | 23 ++++++++
 web/src/lib/types/dialog.ts                   | 14 +++++
 web/src/routes/+layout.svelte                 | 11 ++++
 7 files changed, 161 insertions(+), 4 deletions(-)
 create mode 100644 web/src/components/dialog/DialogHolder.svelte
 create mode 100644 web/src/components/dialog/SmallDialog.svelte
 create mode 100644 web/src/lib/dialogs.ts
 create mode 100644 web/src/lib/types/dialog.ts

diff --git a/web/i18n/en/general.json b/web/i18n/en/general.json
index 6ea5e8a0..5a371ff8 100644
--- a/web/i18n/en/general.json
+++ b/web/i18n/en/general.json
@@ -1,3 +1,4 @@
 {
-    "cobalt": "cobalt"
+    "cobalt": "cobalt",
+    "gotit": "got it"
 }
diff --git a/web/src/components/dialog/DialogHolder.svelte b/web/src/components/dialog/DialogHolder.svelte
new file mode 100644
index 00000000..b5496d65
--- /dev/null
+++ b/web/src/components/dialog/DialogHolder.svelte
@@ -0,0 +1,28 @@
+<script lang="ts">
+    import SmallDialog from "./SmallDialog.svelte";
+    import dialogs from "$lib/dialogs";
+</script>
+
+<div id="dialog-holder" aria-hidden="true">
+    {#each $dialogs as dialog}
+        {#if dialog.type === "small"}
+            <SmallDialog
+                id={dialog.id}
+                title={dialog.title}
+                bodyText={dialog.bodyText}
+                bodySubText={dialog.bodySubText}
+                buttons={dialog.buttons}
+            />
+        {/if}
+    {/each}
+</div>
+
+<style>
+    #dialog-holder {
+        position: absolute;
+        padding-top: env(safe-area-inset-bottom);
+        height: calc(100%);
+        width: 100%;
+        pointer-events: none;
+    }
+</style>
diff --git a/web/src/components/dialog/SmallDialog.svelte b/web/src/components/dialog/SmallDialog.svelte
new file mode 100644
index 00000000..8ee30862
--- /dev/null
+++ b/web/src/components/dialog/SmallDialog.svelte
@@ -0,0 +1,55 @@
+<script lang="ts">
+    import { killDialog } from "$lib/dialogs";
+    import type { DialogButton } from "$lib/types/dialog";
+
+    export let id: string;
+    export let title: string = "";
+    export let bodyText: string = "";
+    export let bodySubText: string = "";
+    export let buttons: DialogButton[];
+
+    let dialogParent: HTMLDialogElement;
+
+    const close = () => {
+        if (dialogParent) {
+            dialogParent.close();
+            killDialog();
+        }
+    }
+
+    $: if (dialogParent) {
+        dialogParent.showModal();
+    }
+</script>
+
+<dialog id="dialog-{id}" bind:this={dialogParent} class="small-dialog">
+    <div class="popup-header">
+        <h2>{title}</h2>
+    </div>
+    <div class="popup-body">
+        {bodyText}
+        {#if bodySubText}
+            <div class="subtext">{bodySubText}</div>
+        {/if}
+    </div>
+    <div class="popup-buttons">
+        {#each buttons as button}
+            <button
+                on:click={
+                    (async() => {
+                        await button.action();
+                        close();
+                    })
+                }
+            >
+                {button.text}
+            </button>
+        {/each}
+    </div>
+</dialog>
+
+<style>
+    .small-dialog {
+        max-width: 375px;
+    }
+</style>
diff --git a/web/src/components/save/buttons/DownloadButton.svelte b/web/src/components/save/buttons/DownloadButton.svelte
index 96510462..718a9041 100644
--- a/web/src/components/save/buttons/DownloadButton.svelte
+++ b/web/src/components/save/buttons/DownloadButton.svelte
@@ -3,14 +3,30 @@
 
     import API from "$lib/api";
     import { device } from "$lib/device";
+
     import { t } from "$lib/i18n/translations";
 
+    import { createDialog } from "$lib/dialogs";
+    import type { DialogInfo } from "$lib/types/dialog";
+
     export let url: string;
 
     $: buttonText = ">>";
     $: buttonAltText = $t('a11y.save.download');
     $: isDisabled = false;
 
+    let defaultErrorPopup = {
+        id: "save-error",
+        type: "small",
+        title: "",
+        bodySubText: "",
+        buttons: [{
+            text: $t("general.gotit"),
+            color: "gray",
+            action: () => {},
+        }]
+    }
+
     const changeDownloadButton = (state: string) => {
         isDisabled = true;
         switch (state) {
@@ -59,14 +75,20 @@
             changeDownloadButton("error");
             restoreDownloadButton();
 
-            return alert("couldn't access the api");
+            return createDialog({
+                ...defaultErrorPopup as DialogInfo,
+                bodyText: "couldn't access the api"
+            })
         }
 
         if (response.status === "error" || response.status === "rate-limit") {
             changeDownloadButton("error");
             restoreDownloadButton();
 
-            return alert(`error from api: ${response.text}`);
+            return createDialog({
+                ...defaultErrorPopup as DialogInfo,
+                bodyText: response.text
+            })
         }
 
         if (response.status === "redirect") {
@@ -90,7 +112,10 @@
                 changeDownloadButton("error");
                 restoreDownloadButton();
 
-                return alert("couldn't probe the stream");
+                return createDialog({
+                    ...defaultErrorPopup as DialogInfo,
+                    bodyText: "couldn't probe the stream"
+                })
             }
         }
     };
diff --git a/web/src/lib/dialogs.ts b/web/src/lib/dialogs.ts
new file mode 100644
index 00000000..a186be04
--- /dev/null
+++ b/web/src/lib/dialogs.ts
@@ -0,0 +1,23 @@
+import { readable, type Updater } from "svelte/store";
+import type { DialogInfo } from "$lib/types/dialog";
+
+let update: (_: Updater<DialogInfo[]>) => void;
+
+export default readable<DialogInfo[]>(
+    [],
+    (_, _update) => { update = _update }
+);
+
+export function createDialog(newData: DialogInfo) {
+    update((popups) => {
+        popups.push(newData);
+        return popups;
+    });
+}
+
+export function killDialog() {
+    update((popups) => {
+        popups.pop()
+        return popups;
+    });
+}
diff --git a/web/src/lib/types/dialog.ts b/web/src/lib/types/dialog.ts
new file mode 100644
index 00000000..b473acd1
--- /dev/null
+++ b/web/src/lib/types/dialog.ts
@@ -0,0 +1,14 @@
+export type DialogButton = {
+    text: string,
+    color: string,
+    action: () => unknown | Promise<unknown>
+}
+
+export type DialogInfo = {
+    id: string,
+    type: "small",
+    title: string,
+    bodyText: string,
+    bodySubText: string,
+    buttons: DialogButton[]
+}
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index 2ba680c5..35306b81 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -8,6 +8,7 @@
 
     import Sidebar from "$components/sidebar/Sidebar.svelte";
     import NotchSticker from "$components/misc/NotchSticker.svelte";
+    import DialogHolder from "$components/dialog/DialogHolder.svelte";
 
     $: reduceMotion =
         $settings.appearance.reduceMotion
@@ -33,6 +34,7 @@
         {#if device.is.iPhone && app.is.installed}
             <NotchSticker />
         {/if}
+        <DialogHolder />
         <Sidebar />
         <div id="content">
             <slot></slot>
@@ -302,6 +304,15 @@
         font-size: 11px;
     }
 
+    :global(dialog) {
+        max-height: 100%;
+        max-width: 100%;
+        padding: var(--padding);
+        border-radius: var(--border-radius);
+        border: none;
+        pointer-events: all;
+    }
+
     :global(.subtext) {
         font-size: 12.5px;
         font-weight: 500;