Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9015d43b9 | ||
|
|
371f5a3814 |
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");
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from mudfish_decky.selfupdate import LATEST_RELEASE_API, latest_plugin_release
|
||||||
|
|
||||||
|
RELEASE = {
|
||||||
|
"tag_name": "v0.1.2",
|
||||||
|
"assets": [
|
||||||
|
{"name": "notes.txt", "browser_download_url": "https://git.example/notes.txt"},
|
||||||
|
{"name": "mudfish-decky-0.1.2.zip",
|
||||||
|
"browser_download_url": "https://git.sejong.dev/ysj7705/mudfish-decky/releases/download/v0.1.2/mudfish-decky-0.1.2.zip"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SelfUpdateTest(unittest.TestCase):
|
||||||
|
def test_reads_latest_release_zip(self):
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
def fetch(url):
|
||||||
|
urls.append(url)
|
||||||
|
return json.dumps(RELEASE)
|
||||||
|
|
||||||
|
self.assertEqual(latest_plugin_release(fetch), {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"url": "https://git.sejong.dev/ysj7705/mudfish-decky/releases/download/v0.1.2/mudfish-decky-0.1.2.zip",
|
||||||
|
})
|
||||||
|
self.assertEqual(urls, [LATEST_RELEASE_API])
|
||||||
|
self.assertEqual(LATEST_RELEASE_API,
|
||||||
|
"https://git.sejong.dev/api/v1/repos/ysj7705/mudfish-decky/releases/latest")
|
||||||
|
|
||||||
|
def test_none_when_release_has_no_zip(self):
|
||||||
|
self.assertIsNone(latest_plugin_release(lambda url: json.dumps({"tag_name": "v1", "assets": []})))
|
||||||
|
|
||||||
|
def test_none_when_unreachable(self):
|
||||||
|
def fetch(url):
|
||||||
|
raise OSError("offline")
|
||||||
|
|
||||||
|
self.assertIsNone(latest_plugin_release(fetch))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user