mirror of
https://github.com/wukko/cobalt.git
synced 2025-05-07 01:04:28 +02:00
347 lines
8.9 KiB
Svelte
347 lines
8.9 KiB
Svelte
<script lang="ts">
|
|
import { page } from "$app/stores";
|
|
import { goto } from "$app/navigation";
|
|
import { browser } from "$app/environment";
|
|
import { SvelteComponent, tick } from "svelte";
|
|
|
|
import { t } from "$lib/i18n/translations";
|
|
|
|
import dialogs from "$lib/dialogs";
|
|
|
|
import { link } from "$lib/state/omnibox";
|
|
import { cachedInfo } from "$lib/api/server-info";
|
|
import { updateSetting } from "$lib/state/settings";
|
|
import { turnstileLoaded } from "$lib/state/turnstile";
|
|
|
|
import type { Optional } from "$lib/types/generic";
|
|
import type { DownloadModeOption } from "$lib/types/settings";
|
|
|
|
import IconLink from "@tabler/icons-svelte/IconLink.svelte";
|
|
import IconLoader2 from "@tabler/icons-svelte/IconLoader2.svelte";
|
|
|
|
import ClearButton from "$components/save/buttons/ClearButton.svelte";
|
|
import DownloadButton from "$components/save/buttons/DownloadButton.svelte";
|
|
|
|
import Switcher from "$components/buttons/Switcher.svelte";
|
|
import ActionButton from "$components/buttons/ActionButton.svelte";
|
|
import SettingsButton from "$components/buttons/SettingsButton.svelte";
|
|
|
|
import IconMute from "$components/icons/Mute.svelte";
|
|
import IconMusic from "$components/icons/Music.svelte";
|
|
import IconSparkles from "$components/icons/Sparkles.svelte";
|
|
import IconClipboard from "$components/icons/Clipboard.svelte";
|
|
|
|
let linkInput: Optional<HTMLInputElement>;
|
|
let downloadButton: SvelteComponent;
|
|
|
|
let isFocused = false;
|
|
let isDisabled = false;
|
|
let isBotCheckOngoing = false;
|
|
let isLoading = false;
|
|
|
|
const validLink = (url: string) => {
|
|
try {
|
|
return /^https:/i.test(new URL(url).protocol);
|
|
} catch {}
|
|
};
|
|
|
|
$: linkFromHash = $page.url.hash.replace("#", "") || "";
|
|
$: linkFromQuery = (browser ? $page.url.searchParams.get("u") : 0) || "";
|
|
|
|
$: if (linkFromHash || linkFromQuery) {
|
|
if (validLink(linkFromHash)) {
|
|
$link = linkFromHash;
|
|
} else if (validLink(linkFromQuery)) {
|
|
$link = linkFromQuery;
|
|
}
|
|
|
|
// clear hash and query to prevent bookmarking unwanted links
|
|
goto("/", { replaceState: true });
|
|
}
|
|
|
|
$: if ($cachedInfo?.info?.cobalt?.turnstileSitekey) {
|
|
if ($turnstileLoaded) {
|
|
isBotCheckOngoing = false;
|
|
} else {
|
|
isBotCheckOngoing = true;
|
|
}
|
|
} else {
|
|
isBotCheckOngoing = false;
|
|
}
|
|
|
|
const pasteClipboard = () => {
|
|
if ($dialogs.length > 0 || isDisabled || isLoading) {
|
|
return;
|
|
}
|
|
|
|
navigator.clipboard.readText().then(async (text: string) => {
|
|
let matchLink = text.match(/https:\/\/[^\s]+/g);
|
|
if (matchLink) {
|
|
$link = matchLink[0];
|
|
|
|
if (!isBotCheckOngoing) {
|
|
await tick(); // wait for button to render
|
|
downloadButton.download($link);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const changeDownloadMode = (mode: DownloadModeOption) => {
|
|
updateSetting({ save: { downloadMode: mode } });
|
|
};
|
|
|
|
const handleKeydown = (e: KeyboardEvent) => {
|
|
if (!linkInput || $dialogs.length > 0 || isDisabled || isLoading) {
|
|
return;
|
|
}
|
|
|
|
if (e.metaKey || e.ctrlKey || e.key === "/") {
|
|
linkInput.focus();
|
|
}
|
|
|
|
if (e.key === "Enter" && validLink($link) && isFocused) {
|
|
downloadButton.download($link);
|
|
}
|
|
|
|
if (["Escape", "Clear"].includes(e.key) && isFocused) {
|
|
$link = "";
|
|
}
|
|
|
|
if (e.target === linkInput) {
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case "D":
|
|
pasteClipboard();
|
|
break;
|
|
case "J":
|
|
changeDownloadMode("auto");
|
|
break;
|
|
case "K":
|
|
changeDownloadMode("audio");
|
|
break;
|
|
case "L":
|
|
changeDownloadMode("mute");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<svelte:window on:keydown={handleKeydown} />
|
|
|
|
<div id="omnibox">
|
|
<div
|
|
id="input-container"
|
|
class:focused={isFocused}
|
|
class:downloadable={validLink($link)}
|
|
>
|
|
<div id="input-link-icon" class:loading={isLoading || isBotCheckOngoing}>
|
|
{#if isLoading || isBotCheckOngoing}
|
|
<IconLoader2 />
|
|
{:else}
|
|
<IconLink />
|
|
{/if}
|
|
</div>
|
|
|
|
<input
|
|
id="link-area"
|
|
bind:value={$link}
|
|
bind:this={linkInput}
|
|
on:input={() => (isFocused = true)}
|
|
on:focus={() => (isFocused = true)}
|
|
on:blur={() => (isFocused = false)}
|
|
spellcheck="false"
|
|
autocomplete="off"
|
|
autocapitalize="off"
|
|
maxlength="512"
|
|
placeholder={$t("save.input.placeholder")}
|
|
aria-label={$t("a11y.save.link_area")}
|
|
data-form-type="other"
|
|
disabled={isDisabled}
|
|
/>
|
|
|
|
{#if $link}
|
|
<ClearButton click={() => ($link = "")} />
|
|
{/if}
|
|
{#if validLink($link)}
|
|
<DownloadButton
|
|
url={$link}
|
|
bind:this={downloadButton}
|
|
bind:disabled={isDisabled}
|
|
bind:loading={isLoading}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
<div id="action-container">
|
|
<Switcher>
|
|
<SettingsButton
|
|
settingContext="save"
|
|
settingId="downloadMode"
|
|
settingValue="auto"
|
|
>
|
|
<IconSparkles />
|
|
{$t("save.auto")}
|
|
</SettingsButton>
|
|
<SettingsButton
|
|
settingContext="save"
|
|
settingId="downloadMode"
|
|
settingValue="audio"
|
|
>
|
|
<IconMusic />
|
|
{$t("save.audio")}
|
|
</SettingsButton>
|
|
<SettingsButton
|
|
settingContext="save"
|
|
settingId="downloadMode"
|
|
settingValue="mute"
|
|
>
|
|
<IconMute />
|
|
{$t("save.mute")}
|
|
</SettingsButton>
|
|
</Switcher>
|
|
|
|
<ActionButton id="paste" click={pasteClipboard}>
|
|
<IconClipboard />
|
|
<span id="paste-desktop-text">{$t("save.paste")}</span>
|
|
<span id="paste-mobile-text">{$t("save.paste.long")}</span>
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
#omnibox {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-width: 640px;
|
|
width: 100%;
|
|
gap: 10px;
|
|
}
|
|
|
|
#input-container {
|
|
display: flex;
|
|
box-shadow: 0 0 0 1.5px var(--input-border) inset;
|
|
border-radius: var(--border-radius);
|
|
padding: 0 10px;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 14px;
|
|
flex: 1;
|
|
}
|
|
|
|
#input-container.downloadable {
|
|
padding-right: 0;
|
|
}
|
|
|
|
#input-container.focused {
|
|
box-shadow: 0 0 0 1.5px var(--secondary) inset;
|
|
outline: var(--secondary) 0.5px solid;
|
|
}
|
|
|
|
#input-link-icon {
|
|
display: flex;
|
|
}
|
|
|
|
#input-link-icon :global(svg) {
|
|
stroke: var(--gray);
|
|
width: 18px;
|
|
height: 18px;
|
|
stroke-width: 2px;
|
|
}
|
|
|
|
#input-link-icon.loading :global(svg) {
|
|
animation: spin 0.7s infinite linear;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
#input-container.focused #input-link-icon :global(svg) {
|
|
stroke: var(--secondary);
|
|
}
|
|
|
|
#input-container.downloadable #input-link-icon :global(svg) {
|
|
stroke: var(--secondary);
|
|
}
|
|
|
|
#link-area {
|
|
display: flex;
|
|
width: 100%;
|
|
margin: 0;
|
|
padding: 10px 0;
|
|
height: 18px;
|
|
|
|
align-items: center;
|
|
|
|
border: none;
|
|
outline: none;
|
|
background-color: transparent;
|
|
color: var(--secondary);
|
|
|
|
-webkit-tap-highlight-color: transparent;
|
|
flex: 1;
|
|
|
|
font-weight: 500;
|
|
|
|
/* workaround for safari */
|
|
font-size: inherit;
|
|
}
|
|
|
|
#link-area:focus-visible {
|
|
box-shadow: unset !important;
|
|
}
|
|
|
|
#link-area::placeholder {
|
|
color: var(--gray);
|
|
/* fix for firefox */
|
|
opacity: 1;
|
|
}
|
|
|
|
/* fix for safari */
|
|
input:disabled {
|
|
opacity: 1;
|
|
}
|
|
|
|
#action-container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
|
|
#action-container {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
#paste-mobile-text {
|
|
display: none;
|
|
}
|
|
|
|
@media screen and (max-width: 440px) {
|
|
#action-container {
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
#action-container :global(.button) {
|
|
width: 100%;
|
|
}
|
|
|
|
#paste-mobile-text {
|
|
display: block;
|
|
}
|
|
|
|
#paste-desktop-text {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|