api & web: merge base queue ui & api updates
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/translations";
|
||||
|
||||
import type { MeowbaltEmotions } from "$lib/types/meowbalt";
|
||||
|
||||
export let emotion: MeowbaltEmotions;
|
||||
|
||||
70
web/src/components/misc/PopoverContainer.svelte
Normal file
70
web/src/components/misc/PopoverContainer.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
export let id = "";
|
||||
export let expanded = false;
|
||||
export let popoverAction: () => void;
|
||||
export let expandStart: "left" | "center" | "right" = "center";
|
||||
|
||||
$: renderPopover = false;
|
||||
|
||||
export const showPopover = async () => {
|
||||
const timeout = !renderPopover;
|
||||
renderPopover = true;
|
||||
|
||||
// 10ms delay to let the popover render for the first time
|
||||
if (timeout) {
|
||||
setTimeout(popoverAction, 10);
|
||||
} else {
|
||||
popoverAction();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div {id} class="popover {expandStart}" aria-hidden={!expanded} class:expanded>
|
||||
{#if renderPopover}
|
||||
<slot></slot>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 18px;
|
||||
background: var(--button);
|
||||
box-shadow:
|
||||
var(--button-box-shadow),
|
||||
0 0 10px 10px var(--popover-glow);
|
||||
|
||||
position: relative;
|
||||
padding: var(--padding);
|
||||
gap: 6px;
|
||||
top: 6px;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform-origin: top center;
|
||||
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.53, 0.05, 0.23, 1.15),
|
||||
opacity 0.25s cubic-bezier(0.53, 0.05, 0.23, 0.99);
|
||||
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.popover.left {
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.popover.center {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.popover.right {
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.popover.expanded {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@
|
||||
export let title: string;
|
||||
export let sectionId: string;
|
||||
export let beta = false;
|
||||
export let nolink = false;
|
||||
export let copyData = "";
|
||||
|
||||
const sectionURL = `${$page.url.origin}${$page.url.pathname}#${sectionId}`;
|
||||
@@ -32,18 +33,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="link-copy"
|
||||
aria-label={copied
|
||||
? $t("button.copied")
|
||||
: $t(`button.copy${copyData ? "" : ".section"}`)}
|
||||
on:click={() => {
|
||||
copied = true;
|
||||
copyURL(copyData || sectionURL);
|
||||
}}
|
||||
>
|
||||
<CopyIcon check={copied} regularIcon={!!copyData} />
|
||||
</button>
|
||||
{#if !nolink}
|
||||
<button
|
||||
class="link-copy"
|
||||
aria-label={copied
|
||||
? $t("button.copied")
|
||||
: $t(`button.copy${copyData ? "" : ".section"}`)}
|
||||
on:click={() => {
|
||||
copied = true;
|
||||
copyURL(copyData || sectionURL);
|
||||
}}
|
||||
>
|
||||
<CopyIcon check={copied} regularIcon={!!copyData} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
pointer-events: all;
|
||||
gap: 8px;
|
||||
margin: var(--padding);
|
||||
margin-right: 71px;
|
||||
margin-top: calc(env(safe-area-inset-top) + var(--padding));
|
||||
box-shadow:
|
||||
var(--button-box-shadow),
|
||||
@@ -90,6 +91,10 @@
|
||||
animation: slide-in-bottom 0.4s;
|
||||
}
|
||||
|
||||
.update-button {
|
||||
margin-right: var(--padding);
|
||||
}
|
||||
|
||||
@keyframes slide-in-bottom {
|
||||
from {
|
||||
transform: translateY(300px);
|
||||
|
||||
187
web/src/components/queue/ProcessingQueue.svelte
Normal file
187
web/src/components/queue/ProcessingQueue.svelte
Normal file
@@ -0,0 +1,187 @@
|
||||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { onNavigate } from "$app/navigation";
|
||||
|
||||
import settings from "$lib/state/settings";
|
||||
import { addToQueue, nukeEntireQueue, queue } from "$lib/state/queue";
|
||||
|
||||
import type { SvelteComponent } from "svelte";
|
||||
import type { QueueItem } from "$lib/types/queue";
|
||||
|
||||
import SectionHeading from "$components/misc/SectionHeading.svelte";
|
||||
import PopoverContainer from "$components/misc/PopoverContainer.svelte";
|
||||
import ProcessingStatus from "$components/queue/ProcessingStatus.svelte";
|
||||
import ProcessingQueueItem from "$components/queue/ProcessingQueueItem.svelte";
|
||||
import ProcessingQueueStub from "$components/queue/ProcessingQueueStub.svelte";
|
||||
|
||||
import IconX from "@tabler/icons-svelte/IconX.svelte";
|
||||
import IconPlus from "@tabler/icons-svelte/IconPlus.svelte";
|
||||
|
||||
import IconGif from "@tabler/icons-svelte/IconGif.svelte";
|
||||
import IconMovie from "@tabler/icons-svelte/IconMovie.svelte";
|
||||
import IconMusic from "@tabler/icons-svelte/IconMusic.svelte";
|
||||
import IconPhoto from "@tabler/icons-svelte/IconPhoto.svelte";
|
||||
import IconVolume3 from "@tabler/icons-svelte/IconVolume3.svelte";
|
||||
|
||||
let popover: SvelteComponent;
|
||||
$: expanded = false;
|
||||
|
||||
$: queueItems = Object.entries($queue) as [id: string, item: QueueItem][];
|
||||
|
||||
$: queueLength = Object.keys($queue).length;
|
||||
$: indeterminate = false;
|
||||
|
||||
const itemIcons = {
|
||||
video: IconMovie,
|
||||
video_mute: IconVolume3,
|
||||
audio: IconMusic,
|
||||
audio_convert: IconMusic,
|
||||
image: IconPhoto,
|
||||
gif: IconGif,
|
||||
};
|
||||
|
||||
const addFakeQueueItem = () => {
|
||||
return addToQueue({
|
||||
id: crypto.randomUUID(),
|
||||
status: "waiting",
|
||||
type: "video",
|
||||
filename: "test.mp4",
|
||||
files: [
|
||||
{
|
||||
type: "video",
|
||||
url: "https://",
|
||||
},
|
||||
],
|
||||
processingSteps: [],
|
||||
});
|
||||
};
|
||||
|
||||
const popoverAction = async () => {
|
||||
expanded = !expanded;
|
||||
};
|
||||
|
||||
onNavigate(() => {
|
||||
expanded = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="processing-queue" class:expanded>
|
||||
<ProcessingStatus {indeterminate} expandAction={popover?.showPopover} />
|
||||
|
||||
<PopoverContainer
|
||||
bind:this={popover}
|
||||
id="processing-popover"
|
||||
{expanded}
|
||||
{popoverAction}
|
||||
expandStart="right"
|
||||
>
|
||||
<div id="processing-header">
|
||||
<SectionHeading
|
||||
title={$t("queue.title")}
|
||||
sectionId="queue"
|
||||
beta
|
||||
nolink
|
||||
/>
|
||||
<div class="header-buttons">
|
||||
{#if queueLength > 0}
|
||||
<button class="clear-button" on:click={nukeEntireQueue}>
|
||||
<IconX />
|
||||
{$t("button.clear")}
|
||||
</button>
|
||||
{/if}
|
||||
<!-- button for ui debug -->
|
||||
{#if $settings.advanced.debug}
|
||||
<button class="test-button" on:click={addFakeQueueItem}>
|
||||
<IconPlus />
|
||||
add item
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div id="processing-list">
|
||||
{#each queueItems as [id, item]}
|
||||
<ProcessingQueueItem
|
||||
{id}
|
||||
filename={item.filename}
|
||||
status={item.status}
|
||||
icon={itemIcons[item.type]}
|
||||
/>
|
||||
{/each}
|
||||
{#if queueLength === 0}
|
||||
<ProcessingQueueStub />
|
||||
{/if}
|
||||
</div>
|
||||
</PopoverContainer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#processing-queue {
|
||||
--holder-padding: 16px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: end;
|
||||
z-index: 9;
|
||||
pointer-events: none;
|
||||
padding: var(--holder-padding);
|
||||
width: calc(100% - var(--holder-padding) * 2);
|
||||
}
|
||||
|
||||
#processing-queue :global(#processing-popover) {
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
width: calc(100% - 16px * 2);
|
||||
max-width: 425px;
|
||||
}
|
||||
|
||||
#processing-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
.header-buttons button {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
text-align: left;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.header-buttons button :global(svg) {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
#processing-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 65vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 535px) {
|
||||
#processing-queue {
|
||||
--holder-padding: 8px;
|
||||
padding-top: 4px;
|
||||
top: env(safe-area-inset-top);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
156
web/src/components/queue/ProcessingQueueItem.svelte
Normal file
156
web/src/components/queue/ProcessingQueueItem.svelte
Normal file
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import IconX from "@tabler/icons-svelte/IconX.svelte";
|
||||
import IconDownload from "@tabler/icons-svelte/IconDownload.svelte";
|
||||
import { removeFromQueue } from "$lib/state/queue";
|
||||
|
||||
export let id: string;
|
||||
export let filename: string;
|
||||
export let status: string;
|
||||
export let progress: number = 0;
|
||||
export let icon: ConstructorOfATypedSvelteComponent;
|
||||
</script>
|
||||
|
||||
<div class="processing-item">
|
||||
<div class="processing-info">
|
||||
<div class="file-title">
|
||||
<div class="processing-type">
|
||||
<svelte:component this={icon} />
|
||||
</div>
|
||||
<span>
|
||||
{filename}
|
||||
</span>
|
||||
</div>
|
||||
<div class="file-progress">
|
||||
<div
|
||||
class="progress"
|
||||
style="width: {Math.min(100, progress)}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="file-status">{id}: {status}</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="action-button">
|
||||
<IconDownload />
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
on:click={() => removeFromQueue(id)}
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.processing-item,
|
||||
.file-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.processing-item {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
gap: 8px;
|
||||
border-bottom: 1.5px var(--button-elevated) solid;
|
||||
}
|
||||
|
||||
.processing-type {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.processing-type :global(svg) {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.processing-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-progress {
|
||||
width: 100%;
|
||||
background-color: var(--button-elevated);
|
||||
}
|
||||
|
||||
.file-progress,
|
||||
.file-progress .progress {
|
||||
height: 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.file-progress .progress {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.file-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
font-size: 12px;
|
||||
color: var(--gray);
|
||||
line-break: anywhere;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.file-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: var(--button);
|
||||
height: 90%;
|
||||
padding-left: 18px;
|
||||
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(0, 0, 0, 1) 20%
|
||||
);
|
||||
}
|
||||
|
||||
.processing-item:hover .file-actions {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 8px;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.action-button :global(svg) {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.processing-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.processing-item:last-child {
|
||||
padding-bottom: 16px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
44
web/src/components/queue/ProcessingQueueStub.svelte
Normal file
44
web/src/components/queue/ProcessingQueueStub.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import Meowbalt from "$components/misc/Meowbalt.svelte";
|
||||
|
||||
const stubActions = ["download", "remux"];
|
||||
|
||||
const randomAction = () => {
|
||||
return stubActions[Math.floor(Math.random() * stubActions.length)];
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="queue-stub">
|
||||
<Meowbalt emotion="think" />
|
||||
<span class="subtext stub-text">
|
||||
{$t("queue.stub", {
|
||||
value: $t(`queue.stub.${randomAction()}`),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.queue-stub {
|
||||
--base-padding: calc(var(--padding) * 1.5);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--gray);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--base-padding);
|
||||
padding-bottom: calc(var(--base-padding) + 16px);
|
||||
text-align: center;
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
.queue-stub :global(.meowbalt) {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.stub-text {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
133
web/src/components/queue/ProcessingStatus.svelte
Normal file
133
web/src/components/queue/ProcessingStatus.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import IconRun from "@tabler/icons-svelte/IconRun.svelte";
|
||||
|
||||
export let indeterminate = false;
|
||||
export let progress: number = 0;
|
||||
export let expandAction: () => void;
|
||||
|
||||
$: progressStroke = `${progress}, 100`;
|
||||
const indeterminateStroke = "15, 5";
|
||||
</script>
|
||||
|
||||
<button
|
||||
id="processing-status"
|
||||
on:click={expandAction}
|
||||
class:completed={progress >= 100}
|
||||
>
|
||||
<svg
|
||||
id="progress-ring"
|
||||
class:indeterminate
|
||||
class:progressive={progress > 0 && !indeterminate}
|
||||
>
|
||||
<circle
|
||||
cx="19"
|
||||
cy="19"
|
||||
r="16"
|
||||
fill="none"
|
||||
stroke-dasharray={indeterminate
|
||||
? indeterminateStroke
|
||||
: progressStroke}
|
||||
/>
|
||||
</svg>
|
||||
<div class="icon-holder">
|
||||
<IconRun />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
#processing-status {
|
||||
--processing-status-glow: 0 0 8px 0px var(--button-elevated-hover);
|
||||
|
||||
pointer-events: all;
|
||||
padding: 7px;
|
||||
border-radius: 30px;
|
||||
box-shadow:
|
||||
var(--button-box-shadow),
|
||||
var(--processing-status-glow);
|
||||
|
||||
transition: box-shadow 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
#processing-status.completed {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--blue) inset,
|
||||
var(--processing-status-glow);
|
||||
}
|
||||
|
||||
:global([data-theme="light"]) #processing-status.completed {
|
||||
background-color: #e0eeff;
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) #processing-status.completed {
|
||||
background-color: #1f3249;
|
||||
}
|
||||
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
background-color: var(--button-elevated-hover);
|
||||
padding: 2px;
|
||||
border-radius: 20px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.icon-holder :global(svg) {
|
||||
height: 21px;
|
||||
width: 21px;
|
||||
stroke: var(--secondary);
|
||||
stroke-width: 1.5px;
|
||||
transition: stroke 0.2s;
|
||||
}
|
||||
|
||||
.completed .icon-holder {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.completed .icon-holder :global(svg) {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
#progress-ring {
|
||||
position: absolute;
|
||||
transform: rotate(-90deg);
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
#progress-ring circle {
|
||||
stroke: var(--blue);
|
||||
stroke-width: 4;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
#progress-ring.progressive circle {
|
||||
transition: stroke-dasharray 0.2s;
|
||||
}
|
||||
|
||||
#progress-ring.progressive,
|
||||
#progress-ring.indeterminate {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#progress-ring.indeterminate {
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
#progress-ring.indeterminate circle {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.completed #progress-ring {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { t } from "$lib/i18n/translations";
|
||||
import { getServerInfo } from "$lib/api/server-info";
|
||||
import cachedInfo from "$lib/state/server-info";
|
||||
import { getServerInfo } from "$lib/api/server-info";
|
||||
|
||||
import type { SvelteComponent } from "svelte";
|
||||
|
||||
import Skeleton from "$components/misc/Skeleton.svelte";
|
||||
import IconPlus from "@tabler/icons-svelte/IconPlus.svelte";
|
||||
import PopoverContainer from "$components/misc/PopoverContainer.svelte";
|
||||
|
||||
let services: string[] = [];
|
||||
|
||||
let popover: HTMLDivElement;
|
||||
|
||||
let popover: SvelteComponent;
|
||||
$: expanded = false;
|
||||
|
||||
let servicesContainer: HTMLDivElement;
|
||||
$: loaded = false;
|
||||
$: renderPopover = false;
|
||||
|
||||
const loadInfo = async () => {
|
||||
await getServerInfo();
|
||||
@@ -29,19 +32,7 @@
|
||||
await loadInfo();
|
||||
}
|
||||
if (expanded) {
|
||||
popover.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const showPopover = async () => {
|
||||
const timeout = !renderPopover;
|
||||
renderPopover = true;
|
||||
|
||||
// 10ms delay to let the popover render for the first time
|
||||
if (timeout) {
|
||||
setTimeout(popoverAction, 10);
|
||||
} else {
|
||||
await popoverAction();
|
||||
servicesContainer.focus();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -49,7 +40,7 @@
|
||||
<div id="supported-services" class:expanded>
|
||||
<button
|
||||
id="services-button"
|
||||
on:click={showPopover}
|
||||
on:click={popover?.showPopover}
|
||||
aria-label={$t(`save.services.title_${expanded ? "hide" : "show"}`)}
|
||||
>
|
||||
<div class="expand-icon">
|
||||
@@ -58,33 +49,36 @@
|
||||
<span class="title">{$t("save.services.title")}</span>
|
||||
</button>
|
||||
|
||||
{#if renderPopover}
|
||||
<div id="services-popover">
|
||||
<div
|
||||
id="services-container"
|
||||
bind:this={popover}
|
||||
tabindex="-1"
|
||||
data-focus-ring-hidden
|
||||
>
|
||||
{#if loaded}
|
||||
{#each services as service}
|
||||
<div class="service-item">{service}</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each { length: 17 } as _}
|
||||
<Skeleton
|
||||
class="elevated"
|
||||
width={Math.random() * 44 + 50 + "px"}
|
||||
height="24.5px"
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div id="services-disclaimer" class="subtext">
|
||||
{$t("save.services.disclaimer")}
|
||||
</div>
|
||||
<PopoverContainer
|
||||
bind:this={popover}
|
||||
id="services-popover"
|
||||
{expanded}
|
||||
{popoverAction}
|
||||
>
|
||||
<div
|
||||
id="services-container"
|
||||
bind:this={servicesContainer}
|
||||
tabindex="-1"
|
||||
data-focus-ring-hidden
|
||||
>
|
||||
{#if loaded}
|
||||
{#each services as service}
|
||||
<div class="service-item">{service}</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each { length: 17 } as _}
|
||||
<Skeleton
|
||||
class="elevated"
|
||||
width={Math.random() * 44 + 50 + "px"}
|
||||
height="24.5px"
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div id="services-disclaimer" class="subtext">
|
||||
{$t("save.services.disclaimer")}
|
||||
</div>
|
||||
</PopoverContainer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -97,34 +91,6 @@
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#services-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 18px;
|
||||
background: var(--button);
|
||||
box-shadow:
|
||||
var(--button-box-shadow),
|
||||
0 0 10px 10px var(--popover-glow);
|
||||
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
gap: 6px;
|
||||
top: 6px;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform-origin: top center;
|
||||
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.53, 0.05, 0.23, 1.15),
|
||||
opacity 0.25s cubic-bezier(0.53, 0.05, 0.23, 0.99);
|
||||
}
|
||||
|
||||
.expanded #services-popover {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#services-button {
|
||||
gap: 9px;
|
||||
padding: 7px 13px 7px 10px;
|
||||
|
||||
@@ -2,9 +2,10 @@ import { defaultLocale } from "$lib/i18n/translations";
|
||||
import type { CobaltSettings } from "$lib/types/settings";
|
||||
|
||||
const defaultSettings: CobaltSettings = {
|
||||
schemaVersion: 4,
|
||||
schemaVersion: 5,
|
||||
advanced: {
|
||||
debug: false,
|
||||
duck: false,
|
||||
},
|
||||
appearance: {
|
||||
theme: "auto",
|
||||
|
||||
70
web/src/lib/state/queue.ts
Normal file
70
web/src/lib/state/queue.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { merge } from "ts-deepmerge";
|
||||
import { get, readable, type Updater } from "svelte/store";
|
||||
import type { OngoingQueueItem, QueueItem } from "$lib/types/queue";
|
||||
|
||||
type Queue = {
|
||||
[id: string]: QueueItem;
|
||||
}
|
||||
|
||||
type OngoingQueue = {
|
||||
[id: string]: OngoingQueueItem;
|
||||
}
|
||||
|
||||
let update: (_: Updater<Queue>) => void;
|
||||
|
||||
const queue = readable<Queue>(
|
||||
{},
|
||||
(_, _update) => { update = _update }
|
||||
);
|
||||
|
||||
export function addToQueue(item: QueueItem) {
|
||||
update(queueData => {
|
||||
queueData[item.id] = item;
|
||||
return queueData;
|
||||
});
|
||||
}
|
||||
|
||||
export function removeFromQueue(id: string) {
|
||||
update(queueData => {
|
||||
delete queueData[id];
|
||||
return queueData;
|
||||
});
|
||||
}
|
||||
|
||||
let updateOngoing: (_: Updater<OngoingQueue>) => void;
|
||||
|
||||
const ongoingQueue = readable<OngoingQueue>(
|
||||
{},
|
||||
(_, _update) => { updateOngoing = _update }
|
||||
);
|
||||
|
||||
export function updateOngoingQueue(id: string, itemInfo: Partial<OngoingQueueItem> = {}) {
|
||||
updateOngoing(queueData => {
|
||||
if (get(queue)?.id) {
|
||||
queueData[id] = merge(queueData[id], {
|
||||
id,
|
||||
...itemInfo,
|
||||
});
|
||||
}
|
||||
|
||||
return queueData;
|
||||
});
|
||||
}
|
||||
|
||||
export function removeFromOngoingQueue(id: string) {
|
||||
updateOngoing(queue => {
|
||||
delete queue[id];
|
||||
return queue;
|
||||
});
|
||||
}
|
||||
|
||||
export function nukeEntireQueue() {
|
||||
update(() => {
|
||||
return {};
|
||||
});
|
||||
updateOngoing(() => {
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
export { queue, ongoingQueue };
|
||||
34
web/src/lib/types/queue.ts
Normal file
34
web/src/lib/types/queue.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
type ProcessingStep = "mux" | "mux_hls" | "encode";
|
||||
type ProcessingPreset = "mp4" | "webm" | "copy";
|
||||
type ProcessingState = "completed" | "failed" | "canceled" | "waiting" | "downloading" | "muxing" | "converting";
|
||||
type ProcessingType = "video" | "video_mute" | "audio" | "audio_convert" | "image" | "gif";
|
||||
type QueueFileType = "video" | "audio" | "image" | "gif";
|
||||
|
||||
export type ProcessingStepItem = {
|
||||
type: ProcessingStep,
|
||||
preset?: ProcessingPreset,
|
||||
}
|
||||
|
||||
export type QueueFile = {
|
||||
type: QueueFileType,
|
||||
url: string,
|
||||
}
|
||||
|
||||
export type QueueItem = {
|
||||
id: string,
|
||||
status: ProcessingState,
|
||||
type: ProcessingType,
|
||||
filename: string,
|
||||
files: QueueFile[],
|
||||
processingSteps: ProcessingStepItem[],
|
||||
}
|
||||
|
||||
export type OngoingQueueItem = {
|
||||
id: string,
|
||||
currentStep?: ProcessingStep,
|
||||
size?: {
|
||||
expected: number,
|
||||
current: number,
|
||||
},
|
||||
speed?: number,
|
||||
}
|
||||
@@ -2,14 +2,16 @@ import type { RecursivePartial } from "$lib/types/generic";
|
||||
import type { CobaltSettingsV2 } from "$lib/types/settings/v2";
|
||||
import type { CobaltSettingsV3 } from "$lib/types/settings/v3";
|
||||
import type { CobaltSettingsV4 } from "$lib/types/settings/v4";
|
||||
import type { CobaltSettingsV5 } from "$lib/types/settings/v5";
|
||||
|
||||
export * from "$lib/types/settings/v2";
|
||||
export * from "$lib/types/settings/v3";
|
||||
export * from "$lib/types/settings/v4";
|
||||
export * from "$lib/types/settings/v5";
|
||||
|
||||
export type CobaltSettings = CobaltSettingsV4;
|
||||
export type CobaltSettings = CobaltSettingsV5;
|
||||
|
||||
export type AnyCobaltSettings = CobaltSettingsV3 | CobaltSettingsV2 | CobaltSettings;
|
||||
export type AnyCobaltSettings = CobaltSettingsV4 | CobaltSettingsV3 | CobaltSettingsV2 | CobaltSettings;
|
||||
|
||||
export type PartialSettings = RecursivePartial<CobaltSettings>;
|
||||
|
||||
|
||||
8
web/src/lib/types/settings/v5.ts
Normal file
8
web/src/lib/types/settings/v5.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { type CobaltSettingsV4 } from "$lib/types/settings/v4";
|
||||
|
||||
export type CobaltSettingsV5 = Omit<CobaltSettingsV4, 'schemaVersion' | 'advanced'> & {
|
||||
schemaVersion: 5,
|
||||
advanced: CobaltSettingsV4['advanced'] & {
|
||||
duck: boolean;
|
||||
};
|
||||
};
|
||||
@@ -24,6 +24,7 @@
|
||||
import Turnstile from "$components/misc/Turnstile.svelte";
|
||||
import NotchSticker from "$components/misc/NotchSticker.svelte";
|
||||
import DialogHolder from "$components/dialog/DialogHolder.svelte";
|
||||
import ProcessingQueue from "$components/queue/ProcessingQueue.svelte";
|
||||
import UpdateNotification from "$components/misc/UpdateNotification.svelte";
|
||||
|
||||
$: reduceMotion =
|
||||
@@ -81,15 +82,18 @@
|
||||
data-reduce-motion={reduceMotion}
|
||||
data-reduce-transparency={reduceTransparency}
|
||||
>
|
||||
{#if $updated}
|
||||
<UpdateNotification />
|
||||
{/if}
|
||||
{#if device.is.iPhone && app.is.installed}
|
||||
<NotchSticker />
|
||||
{/if}
|
||||
<DialogHolder />
|
||||
<Sidebar />
|
||||
{#if $updated}
|
||||
<UpdateNotification />
|
||||
{/if}
|
||||
<div id="content">
|
||||
{#if $settings.advanced.duck}
|
||||
<ProcessingQueue />
|
||||
{/if}
|
||||
{#if ($turnstileEnabled && $page.url.pathname === "/") || $turnstileCreated}
|
||||
<Turnstile />
|
||||
{/if}
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
/>
|
||||
</SettingsCategory>
|
||||
|
||||
<SettingsCategory sectionId="local-processing" title={$t("settings.advanced.duck")} beta>
|
||||
<SettingsToggle
|
||||
settingContext="advanced"
|
||||
settingId="duck"
|
||||
title={$t("settings.advanced.duck.title")}
|
||||
description={$t("settings.advanced.duck.description")}
|
||||
/>
|
||||
</SettingsCategory>
|
||||
|
||||
<SettingsCategory sectionId="data" title={$t("settings.advanced.data")}>
|
||||
<ManageSettings />
|
||||
</SettingsCategory>
|
||||
|
||||
Reference in New Issue
Block a user