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

+2
View File
@@ -49,4 +49,6 @@
3. **Package**: `scripts/package.py`로 `out/mudfish-decky-<버전>.zip`을 만들고 아티팩트로 보관
4. **Release**: `scripts/gitea_release.py`로 Gitea 릴리스 `v<버전>`을 만들고 zip을 올림
덱에 설치된 플러그인을 업데이트하려면 패널의 **고급 ▸ → 최신 플러그인 주소 복사**를 누르세요. 데키 설정 → **Install Plugin from URL**에 붙여 넣으면 됩니다.
이미 같은 버전의 릴리스가 있으면 Release 단계는 아무것도 하지 않습니다. **새 버전을 내려면 `package.json`의 `version`을 올려서 푸시**하면 됩니다. 업로드에는 Jenkins 자격증명 `gitea-ysj7705`(저장소 쓰기 권한이 있는 Gitea 토큰)를 씁니다.
+5 -1
View File
@@ -8,7 +8,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "py_modules"))
import decky # noqa: E402
from mudfish_decky import installer, system # noqa: E402
from mudfish_decky import installer, selfupdate, system # noqa: E402
from mudfish_decky.client import MudfishClient # noqa: E402
from mudfish_decky.controller import Controller # noqa: E402
from mudfish_decky.relay import LoginRelay # noqa: E402
@@ -41,6 +41,10 @@ class Plugin:
async def stop_login(self) -> dict:
return await asyncio.to_thread(self._get_controller().stop_login)
async def plugin_release(self) -> dict:
latest = await asyncio.to_thread(selfupdate.latest_plugin_release)
return {"current": decky.DECKY_PLUGIN_VERSION, "latest": latest}
async def install_mudfish(self) -> dict:
return await asyncio.to_thread(self._get_controller().install)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mudfish-decky",
"version": "0.1.1",
"version": "0.1.2",
"description": "Toggle Mudfish Cloud VPN Full VPN mode from Steam Deck Gaming Mode",
"type": "module",
"scripts": {
+29
View File
@@ -0,0 +1,29 @@
"""Where to get this plugin's newest build: the latest Gitea release's zip (for Decky's Install from URL)."""
import json
import logging
import urllib.request
from typing import Optional
from .client import ssl_context
LATEST_RELEASE_API = "https://git.sejong.dev/api/v1/repos/ysj7705/mudfish-decky/releases/latest"
log = logging.getLogger("mudfish_decky")
def _fetch(url: str) -> str:
with urllib.request.urlopen(url, timeout=10, context=ssl_context()) as resp:
return resp.read().decode()
def latest_plugin_release(fetch=_fetch) -> Optional[dict]:
"""{'version': '0.1.2', 'url': <zip download URL>} or None when offline or no zip is attached."""
try:
release = json.loads(fetch(LATEST_RELEASE_API))
except Exception as e:
log.warning("plugin release lookup failed: %s", e)
return None
zips = [a for a in release.get("assets", []) if a.get("name", "").endswith(".zip")]
if not zips:
return None
return {"version": release.get("tag_name", "").lstrip("v"), "url": zips[0]["browser_download_url"]}
+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");