web/dialogs: add picker dialog & clean up small dialog
This commit is contained in:
parent
24b783e5fb
commit
66bac03e30
11 changed files with 584 additions and 199 deletions
3
web/i18n/en/a11y/dialog.json
Normal file
3
web/i18n/en/a11y/dialog.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"picker.item.generic": "media thumbnail"
|
||||||
|
}
|
|
@ -2,7 +2,14 @@
|
||||||
"button.gotit": "got it",
|
"button.gotit": "got it",
|
||||||
"button.cancel": "cancel",
|
"button.cancel": "cancel",
|
||||||
"button.reset": "reset",
|
"button.reset": "reset",
|
||||||
|
"button.done": "done",
|
||||||
|
"button.downloadAudio": "download audio",
|
||||||
|
|
||||||
"reset.title": "reset all settings?",
|
"reset.title": "reset all settings?",
|
||||||
"reset.body": "are you sure you want to reset all settings? this action is immediate and irreversible."
|
"reset.body": "are you sure you want to reset all settings? this action is immediate and irreversible.",
|
||||||
|
|
||||||
|
"picker.title": "select what to save",
|
||||||
|
"picker.description.desktop": "click an item to save it. images can also be saved via the right click menu.",
|
||||||
|
"picker.description.phone": "press an item to save it. images can also be saved with a long press.",
|
||||||
|
"picker.description.ios": "press an item to save it with a shortcut. images can also be saved with a long press."
|
||||||
}
|
}
|
||||||
|
|
18
web/src/components/dialog/DialogBackdropClose.svelte
Normal file
18
web/src/components/dialog/DialogBackdropClose.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let closeFunc: () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="dialog-backdrop-close"
|
||||||
|
aria-hidden="true"
|
||||||
|
on:click={() => closeFunc()}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#dialog-backdrop-close {
|
||||||
|
position: inherit;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
</style>
|
61
web/src/components/dialog/DialogButtons.svelte
Normal file
61
web/src/components/dialog/DialogButtons.svelte
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DialogButton } from "$lib/types/dialog";
|
||||||
|
|
||||||
|
export let buttons: DialogButton[];
|
||||||
|
export let closeFunc: () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="popup-buttons">
|
||||||
|
{#each buttons as button}
|
||||||
|
<button
|
||||||
|
class="button popup-button {button.color}"
|
||||||
|
class:active={button.main}
|
||||||
|
on:click={async () => {
|
||||||
|
await button.action();
|
||||||
|
closeFunc();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{button.text}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popup-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
gap: calc(var(--padding) / 2);
|
||||||
|
overflow: scroll;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-button.red {
|
||||||
|
background-color: var(--red);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-button:not(.active) {
|
||||||
|
background-color: var(--button-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-button:not(.active):active {
|
||||||
|
background-color: var(--button-elevated-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-button:not(:focus-visible) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.popup-button:not(.active):hover {
|
||||||
|
background-color: var(--button-elevated-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SmallDialog from "$components/dialog/SmallDialog.svelte";
|
|
||||||
import dialogs from "$lib/dialogs";
|
import dialogs from "$lib/dialogs";
|
||||||
|
|
||||||
|
import SmallDialog from "$components/dialog/SmallDialog.svelte";
|
||||||
|
import PickerDialog from "$components/dialog/PickerDialog.svelte";
|
||||||
|
|
||||||
$: backdropVisible = $dialogs.length > 0;
|
$: backdropVisible = $dialogs.length > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -18,11 +20,56 @@
|
||||||
buttons={dialog.buttons}
|
buttons={dialog.buttons}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if dialog.type === "picker"}
|
||||||
|
<PickerDialog
|
||||||
|
id={dialog.id}
|
||||||
|
items={dialog.items}
|
||||||
|
buttons={dialog.buttons}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<div id="dialog-backdrop" class:visible={backdropVisible}></div>
|
<div id="dialog-backdrop" class:visible={backdropVisible}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:global(dialog) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
inset-inline-start: unset;
|
||||||
|
inset-inline-end: unset;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(dialog:modal) {
|
||||||
|
inset-block-start: 0;
|
||||||
|
inset-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(dialog:modal::backdrop) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 535px) {
|
||||||
|
:global(dialog) {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#dialog-holder {
|
#dialog-holder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding-top: env(safe-area-inset-bottom);
|
padding-top: env(safe-area-inset-bottom);
|
||||||
|
@ -60,4 +107,62 @@
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.open .dialog-body) {
|
||||||
|
animation: modal-in 0.35s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.closing .dialog-body) {
|
||||||
|
animation: modal-out 0.15s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 535px) {
|
||||||
|
:global(.open .dialog-body) {
|
||||||
|
animation: modal-in-mobile 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-in {
|
||||||
|
from {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.005);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-in-mobile {
|
||||||
|
from {
|
||||||
|
transform: translateY(200px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
231
web/src/components/dialog/PickerDialog.svelte
Normal file
231
web/src/components/dialog/PickerDialog.svelte
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { tick } from "svelte";
|
||||||
|
import { device } from "$lib/device";
|
||||||
|
import { killDialog } from "$lib/dialogs";
|
||||||
|
import { t } from "$lib/i18n/translations";
|
||||||
|
|
||||||
|
import type { Optional } from "$lib/types/generic";
|
||||||
|
import type { DialogButton } from "$lib/types/dialog";
|
||||||
|
import type { DialogPickerItem } from "$lib/types/dialog";
|
||||||
|
|
||||||
|
import PickerItem from "$components/dialog/PickerItem.svelte";
|
||||||
|
import DialogButtons from "$components/dialog/DialogButtons.svelte";
|
||||||
|
import DialogBackdropClose from "$components/dialog/DialogBackdropClose.svelte";
|
||||||
|
|
||||||
|
import IconBoxMultiple from "@tabler/icons-svelte/IconBoxMultiple.svelte";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let items: Optional<DialogPickerItem[]>;
|
||||||
|
export let buttons: Optional<DialogButton[]>;
|
||||||
|
|
||||||
|
let dialogDescription = "dialog.picker.description.";
|
||||||
|
|
||||||
|
if (device.is.iOS) {
|
||||||
|
dialogDescription += "ios";
|
||||||
|
} else if (device.is.mobile) {
|
||||||
|
dialogDescription += "mobile";
|
||||||
|
} else {
|
||||||
|
dialogDescription += "desktop";
|
||||||
|
}
|
||||||
|
|
||||||
|
let dialogParent: HTMLDialogElement;
|
||||||
|
|
||||||
|
let closing = false;
|
||||||
|
let open = false;
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (dialogParent) {
|
||||||
|
closing = true;
|
||||||
|
open = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
dialogParent.close();
|
||||||
|
killDialog();
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (dialogParent) {
|
||||||
|
dialogParent.showModal();
|
||||||
|
tick().then(() => {
|
||||||
|
open = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
id="dialog-{id}"
|
||||||
|
bind:this={dialogParent}
|
||||||
|
class:closing
|
||||||
|
class:open
|
||||||
|
class:three-columns={items && items.length <= 3}
|
||||||
|
>
|
||||||
|
<div class="dialog-body picker-dialog">
|
||||||
|
<div class="popup-header">
|
||||||
|
<div class="popup-title-container">
|
||||||
|
<IconBoxMultiple />
|
||||||
|
<h2 class="popup-title" tabindex="-1">
|
||||||
|
{$t("dialog.picker.title")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="subtext popup-description">
|
||||||
|
{$t(dialogDescription)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="picker-body">
|
||||||
|
{#if items}
|
||||||
|
{#each items as item}
|
||||||
|
<PickerItem {item} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if buttons}
|
||||||
|
<DialogButtons {buttons} closeFunc={close} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogBackdropClose closeFunc={close} />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.picker-dialog {
|
||||||
|
--dialog-padding: 18px;
|
||||||
|
--picker-item-size: 120px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--padding);
|
||||||
|
max-height: calc(
|
||||||
|
90% - env(safe-area-inset-bottom) - env(safe-area-inset-top)
|
||||||
|
);
|
||||||
|
|
||||||
|
width: auto;
|
||||||
|
background: var(--popup-bg);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px var(--popup-stroke) inset,
|
||||||
|
0 0 60px 10px var(--popup-bg);
|
||||||
|
padding: var(--dialog-padding);
|
||||||
|
position: relative;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
border-radius: 29px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 3px;
|
||||||
|
max-width: calc(var(--picker-item-size) * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--padding) / 2);
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title-container :global(svg) {
|
||||||
|
height: 21px;
|
||||||
|
width: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-description {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title:focus-visible {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-body {
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-columns .picker-body {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-columns .popup-header {
|
||||||
|
max-width: calc(var(--picker-item-size) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.picker-item) {
|
||||||
|
width: var(--picker-item-size);
|
||||||
|
height: var(--picker-item-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 535px) {
|
||||||
|
.picker-dialog {
|
||||||
|
margin-bottom: calc(
|
||||||
|
var(--dialog-padding) + env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 0 2px var(--popup-stroke) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-body {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
max-width: calc(var(--picker-item-size) * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.picker-dialog {
|
||||||
|
--picker-item-size: 115px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 380px) {
|
||||||
|
.picker-dialog {
|
||||||
|
--picker-item-size: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 365px) {
|
||||||
|
.picker-dialog {
|
||||||
|
--picker-item-size: 105px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 350px) {
|
||||||
|
.picker-dialog {
|
||||||
|
--picker-item-size: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 335px) {
|
||||||
|
.picker-body,
|
||||||
|
.three-columns .picker-body {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
max-width: calc(var(--picker-item-size) * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 255px) {
|
||||||
|
.picker-dialog {
|
||||||
|
--picker-item-size: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-body,
|
||||||
|
.three-columns .picker-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
65
web/src/components/dialog/PickerItem.svelte
Normal file
65
web/src/components/dialog/PickerItem.svelte
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from "$lib/i18n/translations";
|
||||||
|
|
||||||
|
import { downloadFile } from "$lib/download";
|
||||||
|
import type { DialogPickerItem } from "$lib/types/dialog";
|
||||||
|
|
||||||
|
import Skeleton from "$components/misc/Skeleton.svelte";
|
||||||
|
|
||||||
|
export let item: DialogPickerItem;
|
||||||
|
|
||||||
|
let imageLoaded = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="picker-item"
|
||||||
|
on:click={() => {
|
||||||
|
downloadFile(item.url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="picker-image"
|
||||||
|
src={item.thumb ? item.thumb : item.url}
|
||||||
|
class:loading={!imageLoaded}
|
||||||
|
on:load={() => (imageLoaded = true)}
|
||||||
|
alt={$t("a11y.dialog.picker.item.generic")}
|
||||||
|
height="100"
|
||||||
|
width="100"
|
||||||
|
/>
|
||||||
|
<Skeleton class="picker-image" hidden={imageLoaded} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.picker-item {
|
||||||
|
background: none;
|
||||||
|
padding: 2px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: calc(var(--border-radius) / 2 + 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.picker-image) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: calc(var(--border-radius) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-image.loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item:active .picker-image {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.picker-item:hover .picker-image {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
|
|
||||||
import { killDialog } from "$lib/dialogs";
|
import { killDialog } from "$lib/dialogs";
|
||||||
import type { DialogButton, SmallDialogIcons } from "$lib/types/dialog";
|
|
||||||
import type { MeowbaltEmotions } from "$lib/types/meowbalt";
|
|
||||||
import type { Optional } from "$lib/types/generic";
|
import type { Optional } from "$lib/types/generic";
|
||||||
|
import type { MeowbaltEmotions } from "$lib/types/meowbalt";
|
||||||
|
import type { DialogButton, SmallDialogIcons } from "$lib/types/dialog";
|
||||||
|
|
||||||
import Meowbalt from "$components/misc/Meowbalt.svelte";
|
import Meowbalt from "$components/misc/Meowbalt.svelte";
|
||||||
|
import DialogButtons from "$components/dialog/DialogButtons.svelte";
|
||||||
|
import DialogBackdropClose from "$components/dialog/DialogBackdropClose.svelte";
|
||||||
|
|
||||||
import IconAlertTriangle from "@tabler/icons-svelte/IconAlertTriangle.svelte";
|
import IconAlertTriangle from "@tabler/icons-svelte/IconAlertTriangle.svelte";
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@
|
||||||
export let title: string = "";
|
export let title: string = "";
|
||||||
export let bodyText: string = "";
|
export let bodyText: string = "";
|
||||||
export let bodySubText: string = "";
|
export let bodySubText: string = "";
|
||||||
export let buttons: DialogButton[];
|
export let buttons: Optional<DialogButton[]>;
|
||||||
|
|
||||||
let dialogParent: HTMLDialogElement;
|
let dialogParent: HTMLDialogElement;
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if title}
|
{#if title}
|
||||||
<h2 id="popup-title" tabindex="-1">{title}</h2>
|
<h2 class="popup-title" tabindex="-1">{title}</h2>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -69,57 +71,15 @@
|
||||||
<div class="subtext">{bodySubText}</div>
|
<div class="subtext">{bodySubText}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="popup-buttons">
|
{#if buttons}
|
||||||
{#each buttons as button}
|
<DialogButtons {buttons} closeFunc={close} />
|
||||||
<button
|
{/if}
|
||||||
class="button popup-button {button.color}"
|
|
||||||
class:active={button.main}
|
|
||||||
on:click={async () => {
|
|
||||||
await button.action();
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{button.text}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dialog-backdrop-close" aria-hidden="true" on:click={() => close()}></div>
|
<DialogBackdropClose closeFunc={close} />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
dialog {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
pointer-events: all;
|
|
||||||
|
|
||||||
inset-inline-start: unset;
|
|
||||||
inset-inline-end: unset;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog:modal {
|
|
||||||
inset-block-start: 0;
|
|
||||||
inset-block-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog:modal::backdrop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-dialog,
|
.small-dialog,
|
||||||
.popup-body {
|
.popup-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -132,34 +92,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-dialog {
|
.small-dialog {
|
||||||
--small-dialog-padding: 18px;
|
--dialog-padding: 18px;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
width: calc(
|
width: calc(
|
||||||
100% - var(--padding) * 2 - var(--small-dialog-padding) * 2
|
100% - var(--padding) * 2 - var(--dialog-padding) * 2
|
||||||
);
|
);
|
||||||
background: var(--popup-bg);
|
background: var(--popup-bg);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 2px var(--popup-stroke) inset,
|
0 0 0 2px var(--popup-stroke) inset,
|
||||||
0 0 60px 10px var(--popup-bg);
|
0 0 60px 10px var(--popup-bg);
|
||||||
padding: var(--small-dialog-padding);
|
padding: var(--dialog-padding);
|
||||||
margin: var(--padding);
|
margin: var(--padding);
|
||||||
border-radius: 29px;
|
border-radius: 29px;
|
||||||
position: relative;
|
position: relative;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open .small-dialog {
|
|
||||||
animation: modal-in 0.35s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closing .small-dialog {
|
|
||||||
animation: modal-out 0.15s;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-dialog.meowbalt-visible {
|
.small-dialog.meowbalt-visible {
|
||||||
padding-top: calc(var(--padding) * 4);
|
padding-top: calc(var(--padding) * 4);
|
||||||
}
|
}
|
||||||
|
@ -169,7 +120,7 @@
|
||||||
top: -120px;
|
top: -120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-header h2 {
|
.popup-title {
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
@ -191,109 +142,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-text:focus-visible,
|
.body-text:focus-visible,
|
||||||
h2:focus-visible {
|
.popup-title:focus-visible {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
gap: calc(var(--padding) / 2);
|
|
||||||
overflow: scroll;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button.red {
|
|
||||||
background-color: var(--red);
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button:not(.active) {
|
|
||||||
background-color: var(--button-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button:not(.active):active {
|
|
||||||
background-color: var(--button-elevated-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button:not(:focus-visible) {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.popup-button:not(.active):hover {
|
|
||||||
background-color: var(--button-elevated-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialog-backdrop-close {
|
|
||||||
position: inherit;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modal-in {
|
|
||||||
from {
|
|
||||||
transform: scale(0.8);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.005);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modal-out {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 535px) {
|
@media screen and (max-width: 535px) {
|
||||||
dialog {
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-dialog {
|
.small-dialog {
|
||||||
margin-bottom: calc(var(--padding) + env(safe-area-inset-bottom));
|
margin-bottom: calc(var(--padding) + env(safe-area-inset-bottom));
|
||||||
box-shadow: 0 0 0 2px var(--popup-stroke) inset;
|
box-shadow: 0 0 0 2px var(--popup-stroke) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open .small-dialog {
|
|
||||||
animation: modal-in-mobile 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modal-in-mobile {
|
|
||||||
from {
|
|
||||||
transform: translateY(200px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
30% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,49 +2,50 @@
|
||||||
import "@fontsource-variable/noto-sans-mono";
|
import "@fontsource-variable/noto-sans-mono";
|
||||||
|
|
||||||
import API from "$lib/api";
|
import API from "$lib/api";
|
||||||
import { device } from "$lib/device";
|
|
||||||
|
|
||||||
import { t } from "$lib/i18n/translations";
|
import { t } from "$lib/i18n/translations";
|
||||||
|
|
||||||
import { createDialog } from "$lib/dialogs";
|
import { createDialog } from "$lib/dialogs";
|
||||||
|
import { downloadFile } from "$lib/download";
|
||||||
|
|
||||||
import type { DialogInfo } from "$lib/types/dialog";
|
import type { DialogInfo } from "$lib/types/dialog";
|
||||||
|
|
||||||
export let url: string;
|
export let url: string;
|
||||||
export let isDisabled = false;
|
export let isDisabled = false;
|
||||||
|
|
||||||
$: buttonText = ">>";
|
$: buttonText = ">>";
|
||||||
$: buttonAltText = $t('a11y.save.download');
|
$: buttonAltText = $t("a11y.save.download");
|
||||||
$: isDisabled = false;
|
$: isDisabled = false;
|
||||||
|
|
||||||
let defaultErrorPopup: DialogInfo = {
|
let defaultErrorPopup: DialogInfo = {
|
||||||
id: "save-error",
|
id: "save-error",
|
||||||
type: "small",
|
type: "small",
|
||||||
meowbalt: "error",
|
meowbalt: "error",
|
||||||
buttons: [{
|
buttons: [
|
||||||
text: $t("dialog.button.gotit"),
|
{
|
||||||
main: true,
|
text: $t("dialog.button.gotit"),
|
||||||
action: () => {},
|
main: true,
|
||||||
}]
|
action: () => {},
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const changeDownloadButton = (state: string) => {
|
const changeDownloadButton = (state: string) => {
|
||||||
isDisabled = true;
|
isDisabled = true;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "think":
|
case "think":
|
||||||
buttonText = "...";
|
buttonText = "...";
|
||||||
buttonAltText = $t('a11y.save.downloadThink');
|
buttonAltText = $t("a11y.save.downloadThink");
|
||||||
break;
|
break;
|
||||||
case "check":
|
case "check":
|
||||||
buttonText = "..?";
|
buttonText = "..?";
|
||||||
buttonAltText = $t('a11y.save.downloadCheck');
|
buttonAltText = $t("a11y.save.downloadCheck");
|
||||||
break;
|
break;
|
||||||
case "done":
|
case "done":
|
||||||
buttonText = ">>>";
|
buttonText = ">>>";
|
||||||
buttonAltText = $t('a11y.save.downloadDone');
|
buttonAltText = $t("a11y.save.downloadDone");
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
buttonText = "!!";
|
buttonText = "!!";
|
||||||
buttonAltText = $t('a11y.save.downloadError');
|
buttonAltText = $t("a11y.save.downloadError");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -53,18 +54,10 @@
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
buttonText = ">>";
|
buttonText = ">>";
|
||||||
isDisabled = false;
|
isDisabled = false;
|
||||||
buttonAltText = $t('a11y.save.download');
|
buttonAltText = $t("a11y.save.download");
|
||||||
}, 2500);
|
}, 2500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadFile = (url: string) => {
|
|
||||||
if (device.is.iOS) {
|
|
||||||
return navigator?.share({ url }).catch(() => {});
|
|
||||||
} else {
|
|
||||||
return window.open(url, "_blank");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// alerts are temporary, we don't have an error popup yet >_<
|
// alerts are temporary, we don't have an error popup yet >_<
|
||||||
export const download = async (link: string) => {
|
export const download = async (link: string) => {
|
||||||
changeDownloadButton("think");
|
changeDownloadButton("think");
|
||||||
|
@ -76,9 +69,9 @@
|
||||||
restoreDownloadButton();
|
restoreDownloadButton();
|
||||||
|
|
||||||
return createDialog({
|
return createDialog({
|
||||||
...defaultErrorPopup as DialogInfo,
|
...(defaultErrorPopup as DialogInfo),
|
||||||
bodyText: "couldn't access the api"
|
bodyText: "couldn't access the api",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === "error" || response.status === "rate-limit") {
|
if (response.status === "error" || response.status === "rate-limit") {
|
||||||
|
@ -86,9 +79,9 @@
|
||||||
restoreDownloadButton();
|
restoreDownloadButton();
|
||||||
|
|
||||||
return createDialog({
|
return createDialog({
|
||||||
...defaultErrorPopup as DialogInfo,
|
...(defaultErrorPopup as DialogInfo),
|
||||||
bodyText: response.text
|
bodyText: response.text,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === "redirect") {
|
if (response.status === "redirect") {
|
||||||
|
@ -113,19 +106,49 @@
|
||||||
restoreDownloadButton();
|
restoreDownloadButton();
|
||||||
|
|
||||||
return createDialog({
|
return createDialog({
|
||||||
...defaultErrorPopup as DialogInfo,
|
...(defaultErrorPopup as DialogInfo),
|
||||||
bodyText: "couldn't probe the stream"
|
bodyText: "couldn't probe the stream",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === "picker") {
|
||||||
|
restoreDownloadButton();
|
||||||
|
|
||||||
|
let pickerButtons = [
|
||||||
|
{
|
||||||
|
text: $t("dialog.button.done"),
|
||||||
|
main: true,
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (response.audio) {
|
||||||
|
const pickerAudio = response.audio;
|
||||||
|
pickerButtons.unshift({
|
||||||
|
text: $t("dialog.button.downloadAudio"),
|
||||||
|
main: false,
|
||||||
|
action: () => {
|
||||||
|
downloadFile(pickerAudio);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return createDialog({
|
||||||
|
id: "download-picker",
|
||||||
|
type: "picker",
|
||||||
|
items: response.picker,
|
||||||
|
buttons: pickerButtons,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
changeDownloadButton("error");
|
changeDownloadButton("error");
|
||||||
restoreDownloadButton();
|
restoreDownloadButton();
|
||||||
|
|
||||||
return createDialog({
|
return createDialog({
|
||||||
...defaultErrorPopup as DialogInfo,
|
...(defaultErrorPopup as DialogInfo),
|
||||||
bodyText: "unknown/unsupported status"
|
bodyText: "unknown/unsupported status",
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
9
web/src/lib/download.ts
Normal file
9
web/src/lib/download.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { device } from "$lib/device";
|
||||||
|
|
||||||
|
export const downloadFile = (url: string) => {
|
||||||
|
if (device.is.iOS) {
|
||||||
|
return navigator?.share({ url }).catch(() => {});
|
||||||
|
} else {
|
||||||
|
return window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
};
|
|
@ -9,13 +9,20 @@ export type DialogButton = {
|
||||||
|
|
||||||
export type SmallDialogIcons = "warn-red";
|
export type SmallDialogIcons = "warn-red";
|
||||||
|
|
||||||
|
export type DialogPickerItem = {
|
||||||
|
type?: 'photo' | 'video',
|
||||||
|
url: string,
|
||||||
|
thumb?: string,
|
||||||
|
}
|
||||||
|
|
||||||
export type DialogInfo = {
|
export type DialogInfo = {
|
||||||
id: string,
|
id: string,
|
||||||
type: "small",
|
type: "small" | "picker",
|
||||||
meowbalt?: MeowbaltEmotions,
|
meowbalt?: MeowbaltEmotions,
|
||||||
icon?: SmallDialogIcons,
|
icon?: SmallDialogIcons,
|
||||||
title?: string,
|
title?: string,
|
||||||
bodyText?: string,
|
bodyText?: string,
|
||||||
bodySubText?: string,
|
bodySubText?: string,
|
||||||
buttons: DialogButton[],
|
buttons?: DialogButton[],
|
||||||
|
items?: DialogPickerItem[],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue