mirror of
https://github.com/nekename/OpenDeck.git
synced 2026-10-28 09:44:01 +00:00
324 lines
11 KiB
Svelte
324 lines
11 KiB
Svelte
<script lang="ts">
|
|
import ArrowClockwise from "phosphor-svelte/lib/ArrowClockwise";
|
|
import ArrowSquareOut from "phosphor-svelte/lib/ArrowSquareOut";
|
|
import CloudArrowDown from "phosphor-svelte/lib/CloudArrowDown";
|
|
import FileArrowUp from "phosphor-svelte/lib/FileArrowUp";
|
|
import Gear from "phosphor-svelte/lib/Gear";
|
|
import MagnifyingGlass from "phosphor-svelte/lib/MagnifyingGlass";
|
|
import Trash from "phosphor-svelte/lib/Trash";
|
|
import WarningCircle from "phosphor-svelte/lib/WarningCircle";
|
|
import ListedPlugin from "./ListedPlugin.svelte";
|
|
import PluginDetails from "./PluginDetails.svelte";
|
|
import Popup from "./Popup.svelte";
|
|
import Tooltip from "./Tooltip.svelte";
|
|
|
|
import { getWebserverUrl } from "$lib/ports";
|
|
import { localisations, settings } from "$lib/settings";
|
|
import { actionList, deviceSelector } from "$lib/singletons";
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
|
|
import { ask, message, open } from "@tauri-apps/plugin-dialog";
|
|
|
|
// @ts-expect-error
|
|
const fetch = window.fetchNative ?? window.fetch;
|
|
|
|
let showPopup: boolean;
|
|
setInterval(async () => {
|
|
if (showPopup) installed = await invoke("list_plugins");
|
|
}, 1e3);
|
|
|
|
async function installPlugin(name: string, url: string | null, file: string | null, fallback_id: string | null) {
|
|
if (!file && !await ask(`It may take a while to install the plugin.`, { title: `Install "${name}"?` })) return;
|
|
try {
|
|
await invoke("install_plugin", { url, file, fallback_id });
|
|
message(`Successfully installed "${name}".`, { title: `Installed "${name}"` });
|
|
$actionList?.reload();
|
|
installed = await invoke("list_plugins");
|
|
} catch (error: any) {
|
|
message(error, { title: `Failed to install "${name}"` });
|
|
}
|
|
}
|
|
|
|
let choices: any[] | undefined;
|
|
let choice: number;
|
|
let finishChoice = (_: unknown) => {};
|
|
let cancelChoice = () => {};
|
|
async function chooseAsset(assets: any[]): Promise<any> {
|
|
choices = assets;
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
finishChoice = resolve;
|
|
cancelChoice = reject;
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
} finally {
|
|
choices = undefined;
|
|
finishChoice = (_: unknown) => {};
|
|
cancelChoice = () => {};
|
|
}
|
|
return assets[choice];
|
|
}
|
|
|
|
let openDetailsView: string | null = null;
|
|
type GitHubPlugin = {
|
|
name: string;
|
|
author: string;
|
|
repository: string;
|
|
download_url: string | undefined;
|
|
};
|
|
async function installPluginGitHub(id: string, plugin: GitHubPlugin) {
|
|
if (plugin.download_url) {
|
|
await installPlugin(plugin.name, plugin.download_url, null, id);
|
|
return;
|
|
}
|
|
|
|
let endpoint = new URL(plugin.repository);
|
|
endpoint.hostname = "api." + endpoint.hostname;
|
|
endpoint.pathname = "/repos" + endpoint.pathname + "/releases";
|
|
|
|
let res;
|
|
try {
|
|
res = await (await fetch(endpoint)).json();
|
|
} catch (error: any) {
|
|
message(error, { title: `Failed to install "${plugin.name}"` });
|
|
return;
|
|
}
|
|
|
|
let assets = [];
|
|
for (const asset of res[0].assets) {
|
|
if (asset.name.toLowerCase().endsWith(".streamdeckplugin") || asset.name.toLowerCase().endsWith(".zip")) {
|
|
assets.push(asset);
|
|
}
|
|
}
|
|
let selected;
|
|
if (assets.length == 1) selected = assets[0];
|
|
else {
|
|
try {
|
|
selected = await chooseAsset(assets);
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
await installPlugin(plugin.name, selected.browser_download_url, null, id);
|
|
}
|
|
|
|
async function installPluginElgato(plugin: any) {
|
|
await installPlugin(plugin.name, `https://plugins.amankhanna.me/rezipped/${plugin.id}.zip`, null, plugin.id);
|
|
}
|
|
|
|
async function installPluginFile() {
|
|
const path = await open({ multiple: false, directory: false });
|
|
if (!path) return;
|
|
await installPlugin(path.split(/[\/\\]/).at(-1) ?? path, null, path, null);
|
|
}
|
|
|
|
async function removePlugin(plugin: any) {
|
|
if (!await ask(`Are you sure you want to remove "${plugin.name}"?`, { title: `Remove "${plugin.name}"?` })) return;
|
|
try {
|
|
await invoke("remove_plugin", { id: plugin.id });
|
|
message(`Successfully removed "${plugin.name}".`, { title: `Removed "${plugin.name}"` });
|
|
$actionList?.reload();
|
|
$deviceSelector?.reloadProfiles();
|
|
installed = await invoke("list_plugins");
|
|
} catch (error: any) {
|
|
message(error, { title: `Failed to remove "${plugin.name}"` });
|
|
}
|
|
}
|
|
|
|
let installed: any[] = [];
|
|
(async () => installed = await invoke("list_plugins"))();
|
|
|
|
let plugins: { [id: string]: GitHubPlugin };
|
|
(async () => plugins = await (await fetch("https://openactionapi.github.io/plugins/catalogue.json")).json())();
|
|
|
|
let query: string = "";
|
|
|
|
onOpenUrl((urls: string[]) => {
|
|
if (!urls[0].includes("installPlugin/")) return;
|
|
let id = urls[0].split("installPlugin/")[1];
|
|
if (!plugins[id]) return;
|
|
installPluginGitHub(id, plugins[id]);
|
|
});
|
|
</script>
|
|
|
|
<button
|
|
class="p-1 w-1/2 text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border dark:border-neutral-600 rounded-lg outline-hidden"
|
|
on:click={() => showPopup = true}
|
|
>
|
|
Plugins
|
|
</button>
|
|
|
|
<svelte:window
|
|
on:keydown={(event) => {
|
|
if (event.key == "Escape") {
|
|
if (choices) cancelChoice();
|
|
else if (openDetailsView) openDetailsView = null;
|
|
else showPopup = false;
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<Popup show={showPopup}>
|
|
<button class="mr-2 my-1 float-right text-xl dark:text-neutral-300" on:click={() => showPopup = false}>✕</button>
|
|
<h2 class="m-2 font-semibold text-xl dark:text-neutral-300">Manage plugins</h2>
|
|
|
|
<h2 class="mx-2 mt-6 mb-2 text-lg dark:text-neutral-400">Installed plugins</h2>
|
|
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{#each installed.sort((a, b) => (a.builtin && !b.builtin) ? -1 : (b.builtin && !a.builtin) ? 1 : a.id.localeCompare(b.id)) as plugin}
|
|
<ListedPlugin
|
|
icon={getWebserverUrl(plugin.icon)}
|
|
name={($localisations && $localisations[plugin.id] && $localisations[plugin.id].Name) ? $localisations[plugin.id].Name : plugin.name}
|
|
subtitle={plugin.version}
|
|
disconnected={!plugin.registered}
|
|
action={() => {
|
|
if ($settings?.developer) invoke("reload_plugin", { id: plugin.id });
|
|
else removePlugin(plugin);
|
|
}}
|
|
secondaryAction={() => {
|
|
if (!plugin.registered) invoke("open_log_directory");
|
|
else if (plugin.has_settings_interface) invoke("show_settings_interface", { plugin: plugin.id });
|
|
}}
|
|
>
|
|
<svelte:fragment slot="secondary">
|
|
{#if !plugin.registered}
|
|
<WarningCircle size="24" color="#E5A50A" />
|
|
{:else if plugin.has_settings_interface}
|
|
<Gear size="24" color="#26A269" />
|
|
{/if}
|
|
</svelte:fragment>
|
|
|
|
{#if $settings?.developer}
|
|
<ArrowClockwise
|
|
size="24"
|
|
class="mt-2"
|
|
color={document.documentElement.classList.contains("dark") ? "#C0BFBC" : "#77767B"}
|
|
/>
|
|
{:else if !plugin.builtin}
|
|
<Trash
|
|
size="24"
|
|
class="mt-2"
|
|
color={document.documentElement.classList.contains("dark") ? "#C0BFBC" : "#77767B"}
|
|
/>
|
|
{/if}
|
|
</ListedPlugin>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="flex flex-row justify-between items-center mx-2 mt-6 mb-2">
|
|
<h2 class="text-lg dark:text-neutral-400">Plugin store</h2>
|
|
<button
|
|
class="flex flex-row items-center mt-2 px-1 py-0.5 text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border dark:border-neutral-600 rounded-lg outline-hidden"
|
|
on:click={installPluginFile}
|
|
>
|
|
<FileArrowUp />
|
|
<span class="ml-1">Install from file</span>
|
|
</button>
|
|
</div>
|
|
<div class="flex flex-row items-center m-2 bg-neutral-200 dark:bg-neutral-700 rounded-md">
|
|
<MagnifyingGlass size="14" class="ml-3 mr-0.5" color={document.documentElement.classList.contains("dark") ? "#DEDDDA" : "#77767B"} />
|
|
<input
|
|
bind:value={query}
|
|
class="w-full p-2 dark:text-neutral-300 outline-hidden"
|
|
placeholder="Search plugins"
|
|
type="search"
|
|
spellcheck="false"
|
|
/>
|
|
</div>
|
|
|
|
<div class="ml-2 mt-8 mb-4">
|
|
<h2 class="font-semibold text-md dark:text-neutral-400">Elgato Marketplace</h2>
|
|
<button
|
|
on:click={() => invoke("open_url", { url: "https://github.com/nekename/OpenDeck/wiki/0.-Elgato-Marketplace" })}
|
|
class="mt-4 text-md text-blue-400 hover:underline"
|
|
>
|
|
Click here for instructions
|
|
</button>
|
|
</div>
|
|
|
|
{#if !plugins}
|
|
<h2 class="mx-2 mt-6 mb-2 text-md dark:text-neutral-400">Loading open-source plugin list...</h2>
|
|
{:else}
|
|
<div class="flex flex-row items-center ml-2 mt-6 mb-2 space-x-2">
|
|
<h2 class="font-semibold text-md dark:text-neutral-400">Open-source plugins</h2>
|
|
<Tooltip> Open-source plugins downloaded from the author's releases. </Tooltip>
|
|
</div>
|
|
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{#each Object.entries(plugins) as [id, plugin]}
|
|
<ListedPlugin
|
|
icon="https://openactionapi.github.io/plugins/icons/{id}.png"
|
|
name={plugin.name}
|
|
subtitle={plugin.author}
|
|
hidden={!plugin.name.toLowerCase().includes(query.toLowerCase())}
|
|
action={() => openDetailsView = id}
|
|
>
|
|
<ArrowSquareOut
|
|
size="24"
|
|
color={document.documentElement.classList.contains("dark") ? "#C0BFBC" : "#77767B"}
|
|
/>
|
|
</ListedPlugin>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
{#await fetch("https://plugins.amankhanna.me/catalogue.json")}
|
|
<h2 class="mx-2 mt-6 mb-2 text-md dark:text-neutral-400">Loading Elgato App Store archive plugin list...</h2>
|
|
{:then archiveRes}
|
|
<div class="flex flex-row items-center mt-6 mb-2">
|
|
<h2 class="mx-2 font-semibold text-md dark:text-neutral-400">Elgato App Store archive</h2>
|
|
<Tooltip> Plugins archived from the Elgato App Store (now replaced by the Elgato Marketplace). </Tooltip>
|
|
</div>
|
|
{#await archiveRes.json() then entries}
|
|
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{#each entries as plugin}
|
|
<ListedPlugin
|
|
icon="https://plugins.amankhanna.me/icons/{plugin.id}.png"
|
|
name={plugin.name}
|
|
subtitle={plugin.author}
|
|
hidden={!plugin.name.toLowerCase().includes(query.toLowerCase())}
|
|
action={() => installPluginElgato(plugin)}
|
|
>
|
|
<CloudArrowDown
|
|
size="24"
|
|
color={document.documentElement.classList.contains("dark") ? "#C0BFBC" : "#77767B"}
|
|
/>
|
|
</ListedPlugin>
|
|
{/each}
|
|
</div>
|
|
{/await}
|
|
{/await}
|
|
</Popup>
|
|
|
|
{#if openDetailsView}
|
|
<PluginDetails
|
|
id={openDetailsView}
|
|
details={plugins[openDetailsView]}
|
|
install={() => {
|
|
// @ts-expect-error
|
|
installPluginGitHub(openDetailsView, plugins[openDetailsView]);
|
|
}}
|
|
close={() => openDetailsView = null}
|
|
/>
|
|
{/if}
|
|
|
|
{#if choices}
|
|
<div class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 mt-2 p-2 w-96 text-xs dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 border-2 dark:border-neutral-600 rounded-lg z-40">
|
|
<h3 class="mb-2 font-semibold text-lg text-center">Choose a release asset</h3>
|
|
<div class="select-wrapper">
|
|
<select class="w-full" bind:value={choice}>
|
|
{#each choices as choice, i}
|
|
<option value={i}>{choice.name}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<button
|
|
class="mt-2 p-1 w-full text-sm text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-800 border dark:border-neutral-600 rounded-lg"
|
|
on:click={finishChoice}
|
|
>
|
|
Install
|
|
</button>
|
|
</div>
|
|
{/if}
|