Spaces:
Paused
Paused
| <script lang="ts"> | |
| import env from "$lib/env"; | |
| 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/state/dialogs"; | |
| import { link } from "$lib/state/omnibox"; | |
| import { updateSetting } from "$lib/state/settings"; | |
| import { pasteLinkFromClipboard } from "$lib/clipboard"; | |
| import { turnstileEnabled, turnstileSolved } from "$lib/state/turnstile"; | |
| import type { Optional } from "$lib/types/generic"; | |
| import type { DownloadModeOption } from "$lib/types/settings"; | |
| import ClearButton from "$components/save/buttons/ClearButton.svelte"; | |
| import DownloadButton from "$components/save/buttons/DownloadButton.svelte"; | |
| import Switcher from "$components/buttons/Switcher.svelte"; | |
| import OmniboxIcon from "$components/save/OmniboxIcon.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 isLoading = false; | |
| $: isBotCheckOngoing = $turnstileEnabled && !$turnstileSolved; | |
| 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 }); | |
| } | |
| const pasteClipboard = async () => { | |
| if ($dialogs.length > 0 || isDisabled || isLoading) { | |
| return; | |
| } | |
| const pastedData = await pasteLinkFromClipboard(); | |
| if (!pastedData) return; | |
| const linkMatch = pastedData.match(/https?\:\/\/[^\s]+/g); | |
| if (linkMatch) { | |
| $link = linkMatch[0].split(',')[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} /> | |
| <!-- | |
| if you want to remove the community instance label, | |
| refer to the license first https://github.com/imputnet/cobalt/tree/main/web#license | |
| --> | |
| {#if env.DEFAULT_API || (!$page.url.host.endsWith(".cobalt.tools") && $page.url.host !== "cobalt.tools")} | |
| <div id="instance-label"> | |
| {$t("save.label.community_instance")} | |
| </div> | |
| {/if} | |
| <div id="omnibox"> | |
| <div | |
| id="input-container" | |
| class:focused={isFocused} | |
| class:downloadable={validLink($link)} | |
| > | |
| <OmniboxIcon loading={isLoading || isBotCheckOngoing} /> | |
| <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={isBotCheckOngoing | |
| ? $t("a11y.save.link_area.turnstile") | |
| : $t("a11y.save.link_area")} | |
| data-form-type="other" | |
| disabled={isDisabled} | |
| /> | |
| {#if $link && !isLoading} | |
| <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: 8px; | |
| } | |
| #input-container { | |
| --input-padding: 10px; | |
| display: flex; | |
| box-shadow: 0 0 0 1.5px var(--input-border) inset; | |
| border-radius: var(--border-radius); | |
| padding: 0 var(--input-padding); | |
| align-items: center; | |
| gap: var(--input-padding); | |
| font-size: 14px; | |
| flex: 1; | |
| } | |
| #input-container.downloadable { | |
| padding-right: 0; | |
| } | |
| #input-container.downloadable:dir(rtl) { | |
| padding-right: var(--input-padding); | |
| padding-left: 0; | |
| } | |
| #input-container.focused { | |
| box-shadow: 0 0 0 1.5px var(--secondary) inset; | |
| outline: var(--secondary) 0.5px solid; | |
| } | |
| #input-container.focused :global(#input-icons svg) { | |
| stroke: var(--secondary); | |
| } | |
| #input-container.downloadable :global(#input-icons svg) { | |
| stroke: var(--secondary); | |
| } | |
| #link-area { | |
| display: flex; | |
| width: 100%; | |
| margin: 0; | |
| padding: var(--input-padding) 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; | |
| } | |
| #instance-label { | |
| font-size: 13px; | |
| color: var(--gray); | |
| font-weight: 500; | |
| } | |
| @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> | |