From d9015d43b973e00bfcdb8128ad8a3efe3fe0b42a Mon Sep 17 00:00:00 2001 From: Sejong Yoo Date: Sun, 25 Oct 2026 01:59:38 +0900 Subject: [PATCH] feat: copy the latest plugin release URL from the panel; release 0.1.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 2 + main.py | 6 ++- package.json | 2 +- py_modules/mudfish_decky/selfupdate.py | 29 +++++++++++++ src/Panel.tsx | 56 ++++++++++++++++++++++++++ src/backend.ts | 6 +++ 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 py_modules/mudfish_decky/selfupdate.py diff --git a/README.md b/README.md index 24df38d..6f62b75 100644 --- a/README.md +++ b/README.md @@ -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 토큰)를 씁니다. diff --git a/main.py b/main.py index 2a2274a..0de6a2b 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/package.json b/package.json index 0a6ab21..77d88b1 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/py_modules/mudfish_decky/selfupdate.py b/py_modules/mudfish_decky/selfupdate.py new file mode 100644 index 0000000..24363cc --- /dev/null +++ b/py_modules/mudfish_decky/selfupdate.py @@ -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': } 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"]} diff --git a/src/Panel.tsx b/src/Panel.tsx index bb6d77c..4a41c70 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -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([]); + const [release, setRelease] = useState(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 ( @@ -242,6 +283,21 @@ export function Panel() { 미꾸라지 제거 + + + {release ? `v${release.current}` : "…"} + {release?.latest + ? isNewer(release.latest.version, release.current) + ? ` · 최신 v${release.latest.version}` + : " (최신)" + : ""} + + + + + 최신 플러그인 주소 복사 + + )} diff --git a/src/backend.ts b/src/backend.ts index f8a4120..a897c9e 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -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");