feat: copy the latest plugin release URL from the panel; release 0.1.2
ysj7705/mudfish-decky/pipeline/head This commit looks good

Advanced shows the installed plugin version and the latest Gitea release.
'최신 플러그인 주소 복사' puts the release zip URL on the clipboard for
Decky's Install Plugin from URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ysj7705andClaude Opus 5.5 committed 2026-10-25 01:59:38 +09:00
1 parent 371f5a3814
commit d9015d43b9
6 files changed
+99 -2

No files matched your search

+56
View File
@@ -8,6 +8,7 @@ import {
showModal,
ToggleField,
} from "@decky/ui";
import { toaster } from "@decky/api";
import { LoginModal } from "./LoginModal";
import { useEffect, useRef, useState } from "react";
import {
@@ -21,6 +22,8 @@ import {
updateMudfish,
NodeInfo,
Phase,
PluginRelease,
pluginRelease,
setNode,
VpnState,
} from "./backend";
@@ -43,6 +46,26 @@ function elapsed(since: number, nowMs: number): string {
return `${Math.floor(minutes / 60)}시간 ${minutes % 60}분째`;
}
/** Copy via a hidden textarea (works in Steam's CEF where navigator.clipboard is often blocked). */
function copyText(text: string): boolean {
const el = document.createElement("textarea");
el.value = text;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.focus();
el.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch {
ok = false;
}
el.remove();
if (!ok) navigator.clipboard?.writeText(text).catch(() => undefined);
return ok;
}
function rttText(rtt: number | null): string {
return rtt === null ? "측정 안 됨" : `${Math.round(rtt)}ms`;
}
@@ -71,6 +94,7 @@ export function Panel() {
const [busy, setBusy] = useState(false);
const [advanced, setAdvanced] = useState(false);
const [nodes, setNodes] = useState<NodeInfo[]>([]);
const [release, setRelease] = useState<PluginRelease | null>(null);
const polling = useRef(false);
const acting = useRef(false);
@@ -94,6 +118,11 @@ export function Panel() {
return () => clearInterval(id);
}, []);
useEffect(() => {
// Fetch ahead of time so the copy happens synchronously inside the button press.
if (advanced) pluginRelease().then(setRelease).catch(() => setRelease(null));
}, [advanced]);
useEffect(() => {
if (!advanced) return;
const load = async () => {
@@ -154,6 +183,18 @@ export function Panel() {
/>,
);
const copyLatestUrl = () => {
const latest = release?.latest;
if (!latest) return;
const copied = copyText(latest.url);
toaster.toast({
title: "Mudfish VPN",
body: copied
? `v${latest.version} 주소를 복사했어요. 데키 설정 → Install Plugin from URL에 붙여 넣으세요`
: `복사하지 못했어요: ${latest.url}`,
});
};
if (missing) {
return (
<PanelSection>
@@ -242,6 +283,21 @@ export function Panel() {
미꾸라지 제거
</ButtonItem>
</PanelSectionRow>
<PanelSectionRow>
<Field label="플러그인" focusable={false}>
{release ? `v${release.current}` : "…"}
{release?.latest
? isNewer(release.latest.version, release.current)
? ` · 최신 v${release.latest.version}`
: " (최신)"
: ""}
</Field>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem layout="below" disabled={!release?.latest} onClick={copyLatestUrl}>
최신 플러그인 주소 복사
</ButtonItem>
</PanelSectionRow>
</>
)}
</PanelSection>
+6
View File
@@ -29,6 +29,12 @@ export const setNode = callable<[choice: string], VpnState>("set_node");
export const listNodes = callable<[], NodeInfo[]>("list_nodes");
export const startLogin = callable<[], VpnState>("start_login");
export const stopLogin = callable<[], VpnState>("stop_login");
export interface PluginRelease {
current: string;
latest: { version: string; url: string } | null;
}
export const pluginRelease = callable<[], PluginRelease>("plugin_release");
export const installMudfish = callable<[], VpnState>("install_mudfish");
export const updateMudfish = callable<[], VpnState>("update_mudfish");
export const uninstallMudfish = callable<[], VpnState>("uninstall_mudfish");