feat: copy the latest plugin release URL from the panel; release 0.1.2
ysj7705/mudfish-decky/pipeline/head This commit looks good
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:
1 parent
371f5a3814
commit
d9015d43b9
6 files changed
+99
-2
No files matched your search
@@ -49,4 +49,6 @@
|
|||||||
3. **Package**: `scripts/package.py`로 `out/mudfish-decky-<버전>.zip`을 만들고 아티팩트로 보관
|
3. **Package**: `scripts/package.py`로 `out/mudfish-decky-<버전>.zip`을 만들고 아티팩트로 보관
|
||||||
4. **Release**: `scripts/gitea_release.py`로 Gitea 릴리스 `v<버전>`을 만들고 zip을 올림
|
4. **Release**: `scripts/gitea_release.py`로 Gitea 릴리스 `v<버전>`을 만들고 zip을 올림
|
||||||
|
|
||||||
|
덱에 설치된 플러그인을 업데이트하려면 패널의 **고급 ▸ → 최신 플러그인 주소 복사**를 누르세요. 데키 설정 → **Install Plugin from URL**에 붙여 넣으면 됩니다.
|
||||||
|
|
||||||
이미 같은 버전의 릴리스가 있으면 Release 단계는 아무것도 하지 않습니다. **새 버전을 내려면 `package.json`의 `version`을 올려서 푸시**하면 됩니다. 업로드에는 Jenkins 자격증명 `gitea-ysj7705`(저장소 쓰기 권한이 있는 Gitea 토큰)를 씁니다.
|
이미 같은 버전의 릴리스가 있으면 Release 단계는 아무것도 하지 않습니다. **새 버전을 내려면 `package.json`의 `version`을 올려서 푸시**하면 됩니다. 업로드에는 Jenkins 자격증명 `gitea-ysj7705`(저장소 쓰기 권한이 있는 Gitea 토큰)를 씁니다.
|
||||||
@@ -8,7 +8,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "py_modules"))
|
|||||||
|
|
||||||
import decky # noqa: E402
|
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.client import MudfishClient # noqa: E402
|
||||||
from mudfish_decky.controller import Controller # noqa: E402
|
from mudfish_decky.controller import Controller # noqa: E402
|
||||||
from mudfish_decky.relay import LoginRelay # noqa: E402
|
from mudfish_decky.relay import LoginRelay # noqa: E402
|
||||||
@@ -41,6 +41,10 @@ class Plugin:
|
|||||||
async def stop_login(self) -> dict:
|
async def stop_login(self) -> dict:
|
||||||
return await asyncio.to_thread(self._get_controller().stop_login)
|
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:
|
async def install_mudfish(self) -> dict:
|
||||||
return await asyncio.to_thread(self._get_controller().install)
|
return await asyncio.to_thread(self._get_controller().install)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mudfish-decky",
|
"name": "mudfish-decky",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Toggle Mudfish Cloud VPN Full VPN mode from Steam Deck Gaming Mode",
|
"description": "Toggle Mudfish Cloud VPN Full VPN mode from Steam Deck Gaming Mode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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"]}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
showModal,
|
showModal,
|
||||||
ToggleField,
|
ToggleField,
|
||||||
} from "@decky/ui";
|
} from "@decky/ui";
|
||||||
|
import { toaster } from "@decky/api";
|
||||||
import { LoginModal } from "./LoginModal";
|
import { LoginModal } from "./LoginModal";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,8 @@ import {
|
|||||||
updateMudfish,
|
updateMudfish,
|
||||||
NodeInfo,
|
NodeInfo,
|
||||||
Phase,
|
Phase,
|
||||||
|
PluginRelease,
|
||||||
|
pluginRelease,
|
||||||
setNode,
|
setNode,
|
||||||
VpnState,
|
VpnState,
|
||||||
} from "./backend";
|
} from "./backend";
|
||||||
@@ -43,6 +46,26 @@ function elapsed(since: number, nowMs: number): string {
|
|||||||
return `${Math.floor(minutes / 60)}시간 ${minutes % 60}분째`;
|
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 {
|
function rttText(rtt: number | null): string {
|
||||||
return rtt === null ? "측정 안 됨" : `${Math.round(rtt)}ms`;
|
return rtt === null ? "측정 안 됨" : `${Math.round(rtt)}ms`;
|
||||||
}
|
}
|
||||||
@@ -71,6 +94,7 @@ export function Panel() {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [advanced, setAdvanced] = useState(false);
|
const [advanced, setAdvanced] = useState(false);
|
||||||
const [nodes, setNodes] = useState<NodeInfo[]>([]);
|
const [nodes, setNodes] = useState<NodeInfo[]>([]);
|
||||||
|
const [release, setRelease] = useState<PluginRelease | null>(null);
|
||||||
const polling = useRef(false);
|
const polling = useRef(false);
|
||||||
const acting = useRef(false);
|
const acting = useRef(false);
|
||||||
|
|
||||||
@@ -94,6 +118,11 @@ export function Panel() {
|
|||||||
return () => clearInterval(id);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!advanced) return;
|
if (!advanced) return;
|
||||||
const load = async () => {
|
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) {
|
if (missing) {
|
||||||
return (
|
return (
|
||||||
<PanelSection>
|
<PanelSection>
|
||||||
@@ -242,6 +283,21 @@ export function Panel() {
|
|||||||
미꾸라지 제거
|
미꾸라지 제거
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
</PanelSectionRow>
|
</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>
|
</PanelSection>
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ export const setNode = callable<[choice: string], VpnState>("set_node");
|
|||||||
export const listNodes = callable<[], NodeInfo[]>("list_nodes");
|
export const listNodes = callable<[], NodeInfo[]>("list_nodes");
|
||||||
export const startLogin = callable<[], VpnState>("start_login");
|
export const startLogin = callable<[], VpnState>("start_login");
|
||||||
export const stopLogin = callable<[], VpnState>("stop_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 installMudfish = callable<[], VpnState>("install_mudfish");
|
||||||
export const updateMudfish = callable<[], VpnState>("update_mudfish");
|
export const updateMudfish = callable<[], VpnState>("update_mudfish");
|
||||||
export const uninstallMudfish = callable<[], VpnState>("uninstall_mudfish");
|
export const uninstallMudfish = callable<[], VpnState>("uninstall_mudfish");
|
||||||
|
|||||||
Reference in new issue
Block a user