diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf5327a..aaf8e09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,25 +17,59 @@ jobs: #build tomoon start - uses: actions/checkout@v4 + - name: Install Tools + run: | + sudo apt-get update + sudo apt-get install -y wget unzip upx + - name: Download Clash and Yacd and Subconverter run: | mkdir tmp && cd tmp mkdir core && cd core # Mihomo (Clash Meta) - wget -O clash.gz https://github.com/MetaCubeX/mihomo/releases/download/v1.19.1/mihomo-linux-amd64-v1.19.1.gz + LATEST_URL=$(curl -s https://api.github.com/repos/MetaCubeX/mihomo/releases/latest | grep "browser_download_url.*linux-amd64-v.*gz\"" | cut -d '"' -f 4) + wget -O clash.gz $LATEST_URL gzip -d clash.gz + + # upx + chmod +x clash + upx clash + # country.mmdb & geosite.dat wget https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb wget https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat wget -O asn.mmdb https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-ASN.mmdb + + # dashboard + mkdir web # yacd - wget -O yacd.zip https://github.com/MetaCubeX/yacd/archive/gh-pages.zip + wget -O yacd.zip https://github.com/haishanh/yacd/archive/refs/heads/gh-pages.zip unzip yacd.zip - mv Yacd-meta-gh-pages web + mv yacd-gh-pages web/yacd + # yacd-meta + wget -O yacd-meta.zip https://github.com/MetaCubeX/yacd/archive/gh-pages.zip + unzip yacd-meta.zip + mv Yacd-meta-gh-pages web/yacd-meta + # metacubexd + wget -O metacubexd.zip https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip + unzip metacubexd.zip + mv metacubexd-gh-pages web/metacubexd + # zashboard + wget -O zashboard.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip + unzip zashboard.zip + mv dist web/zashboard + + echo "clean zips" + rm -f *.zip + cd $GITHUB_WORKSPACE wget -O subconverter_linux64.tar.gz https://github.com/MetaCubeX/subconverter/releases/download/Alpha/subconverter_linux64.tar.gz tar xvf subconverter_linux64.tar.gz + # upx + chmod +x subconverter/subconverter + upx subconverter/subconverter + # build front-end start - uses: actions/setup-node@v4 with: @@ -62,6 +96,10 @@ jobs: args: --target x86_64-unknown-linux-gnu --release use-cross: false + - name: upx tomoon + run: | + upx ./backend/target/x86_64-unknown-linux-gnu/release/tomoon + - name: Collect Files run: | mkdir -p ./release/tomoon/bin/core/web diff --git a/Makefile b/Makefile index 0cfbfe9..fc07405 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,9 @@ help: ## Display list of tasks with descriptions vendor: ## Install project dependencies @echo "+ $@" + @cp -r usdpl src/ @pnpm i + @cd tomoon-web && pnpm i env: ## Create default .env file @echo "+ $@" @@ -38,10 +40,12 @@ download: @echo "+ $@" @$(MAKE) clean_tmp @$(MAKE) download_core + @$(MAKE) upx_core @$(MAKE) download_mmdb - @$(MAKE) download_yacd + @$(MAKE) download_dashboard @$(MAKE) download_rules @$(MAKE) download_subconverter + @$(MAKE) upx_subconverter clean_tmp: @echo "+ $@" @@ -51,9 +55,17 @@ download_core: @echo "+ $@" @mkdir -p ./tmp @mkdir -p ./tmp/core - @wget -O clash.gz https://github.com/MetaCubeX/mihomo/releases/download/v1.19.1/mihomo-linux-amd64-v1.19.1.gz - @gzip -d clash.gz -c > ./tmp/core/clash - @rm -f clash.gz + @echo "Fetching latest release info from GitHub..." + @LATEST_URL=$$(curl -s https://api.github.com/repos/MetaCubeX/mihomo/releases/latest | grep "browser_download_url.*linux-amd64-v.*gz\"" | cut -d '"' -f 4) && \ + echo "Downloading from: $$LATEST_URL" && \ + wget -O clash.gz $$LATEST_URL && \ + gzip -d clash.gz -c > ./tmp/core/clash && \ + rm -f clash.gz + +upx_core: ## Compress core/clash with UPX + @echo "+ $@" + @chmod +x ./tmp/core/clash + @upx ./tmp/core/clash download_mmdb: @echo "+ $@" @@ -63,17 +75,50 @@ download_mmdb: @wget -O ./tmp/core/geosite.dat https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat @wget -O ./tmp/core/asn.mmdb https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-ASN.mmdb -download_yacd: +download_dashboard: @echo "+ $@" @mkdir -p ./tmp @mkdir -p ./tmp/core - @wget -O ./tmp/yacd.zip https://github.com/MetaCubeX/yacd/archive/gh-pages.zip - @unzip ./tmp/yacd.zip -d ./tmp - @mv ./tmp/Yacd-meta-gh-pages ./tmp/core/web - @rm -f ./tmp/yacd.zip + @mkdir -p ./tmp/core/web + @rm -rf ./tmp/core/web/* + + $(MAKE) download_yacd + $(MAKE) download_yacd_meta + $(MAKE) download_metacubexd + $(MAKE) download_zashboard + + @echo "clean tmp" + @rm -f ./tmp/*.zip + +download_yacd: + @echo "+ $@" + @wget -O ./tmp/yacd.zip https://github.com/haishanh/yacd/archive/refs/heads/gh-pages.zip + @unzip -o ./tmp/yacd.zip -d ./tmp + @mv ./tmp/yacd-gh-pages ./tmp/core/web/yacd + +download_yacd_meta: + @echo "+ $@" + @wget -O ./tmp/yacd-meta.zip https://github.com/MetaCubeX/yacd/archive/gh-pages.zip + @unzip -o ./tmp/yacd-meta.zip -d ./tmp + @mv ./tmp/Yacd-meta-gh-pages ./tmp/core/web/yacd-meta + +download_metacubexd: + @echo "+ $@" + @wget -O ./tmp/metacubexd.zip https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip + @unzip -o ./tmp/metacubexd.zip -d ./tmp + @mv ./tmp/metacubexd-gh-pages ./tmp/core/web/metacubexd + +download_zashboard: + @echo "+ $@" + @wget -O ./tmp/zashboard.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip + @unzip -o ./tmp/zashboard.zip -d ./tmp + @mv ./tmp/dist ./tmp/core/web/zashboard + download_rules: @echo "+ $@" + @mkdir -p ./assets/subconverter_rules + @rm -rf ./assets/subconverter_rules/*.list* @bash ./assets/subconverter_rules/dl_rules.sh ./assets/subconverter_rules download_subconverter: @@ -84,6 +129,11 @@ download_subconverter: @tar xvf subconverter_linux64.tar.gz -C ./tmp/subconverter_tmp/ @cp ./tmp/subconverter_tmp/subconverter/subconverter ./tmp/subconverter/ @rm -r ./tmp/subconverter_tmp + +upx_subconverter: ## Compress subconverter with UPX + @echo "+ $@" + @chmod +x ./tmp/subconverter/subconverter + @upx ./tmp/subconverter/subconverter build-front: ## Build frontend @echo "+ $@" @@ -101,7 +151,9 @@ build-front-sub: copy-file: @echo "+ $@" - @cp -r ./tmp/core ./bin/ + @mkdir -p ./bin + @rm -rf ./bin/core + @cp -rv ./tmp/core ./bin/ @cp ./tmp/subconverter/subconverter ./bin/subconverter build-back: ## Build backend @@ -127,7 +179,6 @@ deploy-steamdeck: ## Deploy plugin build to steamdeck --exclude='node_modules/' \ --exclude='.pnpm-store/' \ --exclude='src/' \ - --exclude='yacd' . \ --exclude='tomoon-web/' \ --exclude='backend/' \ --exclude='tmp/' \ diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c44aae7..f1cbbaa 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -24,4 +24,4 @@ env_logger = "0.10.0" local-ip-address = "0.5.1" actix-cors = "0.6.4" tokio = { version = "1.24.1", features = ["macros", "process"]} -urlencoding = "2.1.3" +urlencoding = "2.1.3" \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile index ff37be2..56b2199 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -2,8 +2,8 @@ all: @echo "+ $@" @rustup default stable - # @cross build --release --target x86_64-unknown-linux-gnu - @cargo build --release --target x86_64-unknown-linux-gnu + @cross build --release --target x86_64-unknown-linux-gnu + # @cargo build --release --target x86_64-unknown-linux-gnu @mkdir -p ../bin @cp ./target/x86_64-unknown-linux-gnu/release/tomoon ../bin/tomoon diff --git a/backend/src/api.rs b/backend/src/api.rs index b5adbf6..faa1776 100755 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -83,7 +83,14 @@ pub fn set_clash_status(runtime: &ControlRuntime) -> impl Fn(Vec) -> } } if *enabled { - match clash.run(&settings.current_sub, settings.skip_proxy, settings.override_dns, settings.enhanced_mode) { + match clash.run( + &settings.current_sub, + settings.skip_proxy, + settings.override_dns, + settings.allow_remote_access, + settings.enhanced_mode, + settings.dashboard.clone(), + ) { Ok(_) => (), Err(e) => { log::error!("Run clash error: {}", e); @@ -190,7 +197,9 @@ pub fn download_sub(runtime: &ControlRuntime) -> impl Fn(Vec) -> Vec< } }; if !helper::check_yaml(&file_content) { - log::error!("The downloaded subscription is not a legal profile."); + log::error!( + "The downloaded subscription is not a legal profile." + ); update_status(DownloadStatus::Error); return; } @@ -249,14 +258,19 @@ pub fn download_sub(runtime: &ControlRuntime) -> impl Fn(Vec) -> Vec< // 是一个链接 } else { match minreq::get(url.clone()) - .with_header("User-Agent", format!("ToMoonClash/{}",env!("CARGO_PKG_VERSION"))) + .with_header( + "User-Agent", + format!("ToMoonClash/{}", env!("CARGO_PKG_VERSION")), + ) .with_timeout(15) .send() { Ok(x) => { let response = x.as_str().unwrap(); if !helper::check_yaml(&String::from(response)) { - log::error!("The downloaded subscription is not a legal profile."); + log::error!( + "The downloaded subscription is not a legal profile." + ); update_status(DownloadStatus::Error); return; } @@ -446,7 +460,7 @@ pub fn delete_sub(runtime: &ControlRuntime) -> impl Fn(Vec) -> Vec impl Fn(Vec) -> Vec { - //let runtime_clash = runtime.clash_state_clone(); + let runtime_clash = runtime.clash_state_clone(); let runtime_state = runtime.state_clone(); let runtime_setting = runtime.settings_clone(); move |params: Vec| { @@ -471,16 +485,16 @@ pub fn set_sub(runtime: &ControlRuntime) -> impl Fn(Vec) -> Vec { - // x.update_config_path(path); - // log::info!("set profile path to {}", path); - // } - // Err(e) => { - // log::error!("set_sub() failed to acquire clash write lock: {}", e); - // } - // } + //更新到当前内存中 + match runtime_clash.write() { + Ok(mut x) => { + x.update_config_path(path); + log::info!("set profile path to {}", path); + } + Err(e) => { + log::error!("set_sub() failed to acquire clash write lock: {}", e); + } + } } return vec![]; } @@ -505,9 +519,16 @@ pub fn update_subs(runtime: &ControlRuntime) -> impl Fn(Vec) -> Vec

{ let response = match response.as_str() { Ok(x) => x, @@ -517,7 +538,9 @@ pub fn update_subs(runtime: &ControlRuntime) -> impl Fn(Vec) -> Vec

impl Fn(Vec) -> Vec

{ log::error!("Error occurred while download sub {}", i.url); log::error!("Error Message : {}", e); @@ -575,14 +598,11 @@ pub fn create_debug_log(runtime: &ControlRuntime) -> impl Fn(Vec) -> //let update_status = runtime.update_status_clone(); let home = match runtime.state_clone().read() { Ok(state) => state.home.clone(), - Err(_) => State::default().home + Err(_) => State::default().home, }; move |_| { - let running_status = format!( - "Clash status : {}\n", - helper::is_clash_running() - ); - let tomoon_config =match fs::read_to_string(home.join(".config/tomoon/tomoon.json")) { + let running_status = format!("Clash status : {}\n", helper::is_clash_running()); + let tomoon_config = match fs::read_to_string(home.join(".config/tomoon/tomoon.json")) { Ok(x) => x, Err(e) => { format!("can not get Tomoon config, error message: {} \n", e) @@ -611,10 +631,7 @@ pub fn create_debug_log(runtime: &ControlRuntime) -> impl Fn(Vec) -> Clash log:\n {}\n ", - running_status, - tomoon_config, - tomoon_log, - clash_log, + running_status, tomoon_config, tomoon_log, clash_log, ); fs::write("/tmp/tomoon.debug.log", log).unwrap(); return vec![true.into()]; diff --git a/backend/src/control.rs b/backend/src/control.rs index 084233c..dbd9fe9 100755 --- a/backend/src/control.rs +++ b/backend/src/control.rs @@ -12,6 +12,8 @@ use serde_yaml::{Mapping, Value}; use super::helper; use super::settings::{Settings, State}; +use serde_json::json; + pub struct ControlRuntime { settings: Arc>, state: Arc>, @@ -249,15 +251,21 @@ impl Default for Clash { } impl Clash { - pub fn run(&mut self, config_path: &String, skip_proxy: bool, override_dns: bool, enhanced_mode: EnhancedMode) -> Result<(), ClashError> { - // decky 插件数据目录 + pub fn run( + &mut self, + config_path: &String, + skip_proxy: bool, + override_dns: bool, + allow_remote_access: bool, + enhanced_mode: EnhancedMode, + dashboard: String, + ) -> Result<(), ClashError> { + // decky 插件数据目录 let decky_data_dir = get_decky_data_dir().unwrap(); let new_country_db_path = get_current_working_dir() .unwrap() .join("bin/core/country.mmdb"); - let new_asn_db_path = get_current_working_dir() - .unwrap() - .join("bin/core/asn.mmdb"); + let new_asn_db_path = get_current_working_dir().unwrap().join("bin/core/asn.mmdb"); let new_geosite_path = get_current_working_dir() .unwrap() .join("bin/core/geosite.dat"); @@ -272,13 +280,10 @@ impl Clash { // 检查数据库文件是否存在,不存在则复制 if !PathBuf::from(country_db_path.clone()).is_file() { - match fs::copy( - new_country_db_path.clone(), - country_db_path.clone(), - ) { + match fs::copy(new_country_db_path.clone(), country_db_path.clone()) { Ok(_) => { log::info!("Copy country.mmdb to decky data dir") - }, + } Err(e) => { return Err(ClashError { Message: e.to_string(), @@ -289,13 +294,10 @@ impl Clash { } if !PathBuf::from(asn_db_path.clone()).is_file() { - match fs::copy( - new_asn_db_path.clone(), - asn_db_path.clone(), - ) { + match fs::copy(new_asn_db_path.clone(), asn_db_path.clone()) { Ok(_) => { log::info!("Copy asn.mmdb to decky data dir") - }, + } Err(e) => { return Err(ClashError { Message: e.to_string(), @@ -304,15 +306,12 @@ impl Clash { } } } - + if !PathBuf::from(geosite_path.clone()).is_file() { - match fs::copy( - new_geosite_path.clone(), - geosite_path.clone(), - ) { + match fs::copy(new_geosite_path.clone(), geosite_path.clone()) { Ok(_) => { log::info!("Copy geosite.dat to decky data dir") - }, + } Err(e) => { return Err(ClashError { Message: e.to_string(), @@ -324,7 +323,13 @@ impl Clash { self.update_config_path(config_path); // 修改配置文件为推荐配置 - match self.change_config(skip_proxy, override_dns, enhanced_mode) { + match self.change_config( + skip_proxy, + override_dns, + allow_remote_access, + enhanced_mode, + dashboard, + ) { Ok(_) => (), Err(e) => { return Err(ClashError { @@ -336,7 +341,7 @@ impl Clash { //log::info!("Pre-setting network"); //TODO: 未修改的 unwarp - let run_config = decky_data_dir.join("running_config.yaml"); + let run_config = self.get_running_config().unwrap(); let outputs = fs::File::create("/tmp/tomoon.clash.log").unwrap(); let errors = outputs.try_clone().unwrap(); @@ -384,24 +389,117 @@ impl Clash { self.config = std::path::PathBuf::from((*path).clone()); } - pub fn change_config(&self, skip_proxy: bool, override_dns: bool, enhanced_mode: EnhancedMode) -> Result<(), Box> { + pub fn get_running_config(&self) -> std::io::Result { + let decky_data_dir = get_decky_data_dir().unwrap(); + let run_config = decky_data_dir.join("running_config.yaml"); + Ok(run_config) + } + + pub async fn reload_config(&self) -> Result<(), ClashError> { + let run_config = self.get_running_config().unwrap(); + log::info!("Reloading Clash config, config: {}", run_config.display()); + + let url = "http://127.0.0.1:9090/configs?reload=true"; + let body = json!({ + "path": run_config, + "payload": "" + }); + + let body_str = serde_json::to_string(&body).unwrap(); + + let res = match minreq::put(url) + .with_header("Content-Type", "application/json") + .with_body(body_str) + .send() + { + Ok(x) => x, + Err(e) => { + log::error!("Failed to restart Clash core: {}", e); + return Err(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + }); + } + }; + + if res.status_code == 200 || res.status_code == 204 { + log::info!("Clash config reloaded successfully"); + } else { + log::error!( + "Failed to reload Clash config, status_code {}", + res.status_code + ); + } + + Ok(()) + } + + pub async fn restart_core(&self) -> Result<(), ClashError> { + log::info!("Restarting Clash core..."); + + let url = "http://127.0.0.1:9090/restart"; + let body = json!({ + "payload": "" + }); + let body_str = serde_json::to_string(&body).unwrap(); + + let res = match minreq::post(url) + .with_header("Content-Type", "application/json") + .with_body(body_str) + .send() + { + Ok(x) => x, + Err(e) => { + log::error!("Failed to restart Clash core: {}", e); + return Err(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + }); + } + }; + + if res.status_code == 200 { + log::info!("Clash restart successfully"); + } else { + let data = res.as_str().unwrap(); + log::error!("Failed to restart Clash core: {}", data); + } + + Ok(()) + } + + pub fn change_config( + &self, + skip_proxy: bool, + override_dns: bool, + allow_remote_access: bool, + enhanced_mode: EnhancedMode, + dashboard: String, + ) -> Result<(), Box> { let path = self.config.clone(); + log::info!("change_config path: {:?}", path); + let config = fs::read_to_string(path)?; let mut yaml: serde_yaml::Value = serde_yaml::from_str(config.as_str())?; let yaml = yaml.as_mapping_mut().unwrap(); log::info!("Changing Clash config..."); - //修改 WebUI + let external_ip = if allow_remote_access { + "0.0.0.0" + } else { + "127.0.0.1" + }; + //修改 WebUI match yaml.get_mut("external-controller") { Some(x) => { - *x = Value::String(String::from("127.0.0.1:9090")); + *x = Value::String(String::from(format!("{}:9090", external_ip))); } None => { yaml.insert( Value::String(String::from("external-controller")), - Value::String(String::from("127.0.0.1:9090")), + Value::String(String::from(format!("{}:9090", external_ip))), ); } } @@ -417,13 +515,13 @@ impl Clash { if skip_proxy { rules.insert( - 0, - Value::String(String::from("DOMAIN-SUFFIX,cm.steampowered.com,DIRECT")), - ); - rules.insert( - 0, - Value::String(String::from("DOMAIN-SUFFIX,steamserver.net,DIRECT")), - ); + 0, + Value::String(String::from("DOMAIN-SUFFIX,cm.steampowered.com,DIRECT")), + ); + rules.insert( + 0, + Value::String(String::from("DOMAIN-SUFFIX,steamserver.net,DIRECT")), + ); } } @@ -442,6 +540,19 @@ impl Clash { } } + // 修改 dashboard 名称 + match yaml.get_mut("external-ui-name") { + Some(x) => { + *x = Value::String(String::from(dashboard)); + } + None => { + yaml.insert( + Value::String(String::from("external-ui-name")), + Value::String(String::from(dashboard)), + ); + } + } + //修改 TUN 和 DNS 配置 let tun_config = " @@ -558,6 +669,18 @@ impl Clash { } } + // // 如果设置了 secret, 更改 secret 为 "tomoon" + // let secret_config = "tomoon"; + // match yaml.get("secret") { + // Some(_) => { + // yaml.remove("secret").unwrap(); + // insert_config(yaml, secret_config, "secret"); + // } + // None => { + // insert_config(yaml, secret_config, "secret"); + // } + // } + // 保存上次的配置 match yaml.get("profile") { Some(_) => { @@ -569,13 +692,33 @@ impl Clash { } } - let run_config = get_decky_data_dir()?.join("running_config.yaml"); + let run_config = self.get_running_config()?; let yaml_str = serde_yaml::to_string(&yaml)?; - fs::write(run_config, yaml_str)?; + + match fs::write(run_config, yaml_str) { + Ok(_) => { + log::info!("Clash config changed successfully"); + } + Err(e) => { + log::error!("Error occurred while changing Clash config: {}", e); + } + } log::info!("Clash config changed successfully"); Ok(()) } + pub fn get_running_secret(&self) -> Result> { + let path = self.get_running_config()?; + let content = std::fs::read_to_string(path)?; + let yaml: serde_yaml::Value = serde_yaml::from_str(&content)?; + + match yaml.get("secret") { + Some(secret) => { + Ok(secret.as_str().unwrap_or("").to_string()) + } + None => Ok("".to_string()), + } + } } diff --git a/backend/src/external_web.rs b/backend/src/external_web.rs index c4b4687..ce737cd 100644 --- a/backend/src/external_web.rs +++ b/backend/src/external_web.rs @@ -2,16 +2,17 @@ use actix_web::{body::BoxBody, web, HttpResponse, Result}; use local_ip_address::local_ip; use rand::{distributions::Alphanumeric, Rng}; use serde::{Deserialize, Serialize}; -use tokio::net::TcpStream; -use tokio::time::sleep; use std::time::Duration; use std::{collections::HashMap, fs, path::PathBuf, sync::Mutex}; +use tokio::net::TcpStream; use tokio::process::Command; use tokio::sync::mpsc; +use tokio::time::sleep; use crate::{ control::{ClashError, ClashErrorKind, EnhancedMode}, - helper, settings::State, + helper, + settings::State, }; pub struct Runtime(pub *const crate::control::ControlRuntime); @@ -25,7 +26,7 @@ pub struct AppState { #[derive(Deserialize)] pub struct GenLinkParams { link: String, - subconv: bool + subconv: bool, } #[derive(Deserialize)] @@ -33,6 +34,11 @@ pub struct SkipProxyParams { skip_proxy: bool, } +#[derive(Deserialize)] +pub struct AllowRemoteAccessParams { + allow_remote_access: bool, +} + #[derive(Deserialize)] pub struct OverrideDNSParams { override_dns: bool, @@ -43,6 +49,11 @@ pub struct EnhancedModeParams { enhanced_mode: EnhancedMode, } +#[derive(Deserialize)] +pub struct DashboardParams { + dashboard: String, +} + #[derive(Serialize, Deserialize)] pub struct GenLinkResponse { status_code: u16, @@ -61,6 +72,18 @@ pub struct OverrideDNSResponse { message: String, } +#[derive(Serialize, Deserialize)] +pub struct AllowRemoteAccessResponse { + status_code: u16, + message: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DashboardResponse { + status_code: u16, + message: String, +} + #[derive(Deserialize)] pub struct GetLinkParams { code: u16, @@ -77,6 +100,9 @@ pub struct GetConfigResponse { skip_proxy: bool, override_dns: bool, enhanced_mode: EnhancedMode, + allow_remote_access: bool, + dashboard: String, + secret: String, } #[derive(Serialize, Deserialize)] @@ -193,6 +219,54 @@ pub async fn override_dns( Ok(HttpResponse::Ok().json(r)) } +// allow_remote_access +pub async fn allow_remote_access( + state: web::Data, + params: web::Form, +) -> Result { + let allow_remote_access = params.allow_remote_access.clone(); + let runtime = state.runtime.lock().unwrap(); + let runtime_settings; + let runtime_state; + unsafe { + let runtime = runtime.0.as_ref().unwrap(); + runtime_settings = runtime.settings_clone(); + runtime_state = runtime.state_clone(); + } + match runtime_settings.write() { + Ok(mut x) => { + x.allow_remote_access = allow_remote_access; + let mut state = match runtime_state.write() { + Ok(x) => x, + Err(e) => { + log::error!( + "allow_remote_access failed to acquire state write lock: {}", + e + ); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + }; + state.dirty = true; + } + Err(e) => { + log::error!("Failed while toggle allow_remote_access."); + log::error!("Error Message:{}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::ConfigNotFound, + })); + } + } + let r = OverrideDNSResponse { + message: "修改成功".to_string(), + status_code: 200, + }; + Ok(HttpResponse::Ok().json(r)) +} + pub async fn enhanced_mode( state: web::Data, params: web::Form, @@ -237,19 +311,228 @@ pub async fn enhanced_mode( Ok(HttpResponse::Ok().json(r)) } +// set_dashboard +pub async fn set_dashboard( + state: web::Data, + params: web::Form, +) -> Result { + let dashboard = params.dashboard.clone(); + let runtime = state.runtime.lock().unwrap(); + let runtime_settings; + let runtime_state; + unsafe { + let runtime = runtime.0.as_ref().unwrap(); + runtime_settings = runtime.settings_clone(); + runtime_state = runtime.state_clone(); + } + + match runtime_settings.write() { + Ok(mut x) => { + x.dashboard = dashboard; + let mut state = match runtime_state.write() { + Ok(x) => x, + Err(e) => { + log::error!("set_dashboard failed to acquire state write lock: {}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + }; + state.dirty = true; + } + Err(e) => { + log::error!("Failed while set dashboard."); + log::error!("Error Message:{}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::ConfigNotFound, + })); + } + } + let r = DashboardResponse { + message: "修改成功".to_string(), + status_code: 200, + }; + Ok(HttpResponse::Ok().json(r)) +} + +pub async fn restart_clash(state: web::Data) -> Result { + let runtime = state.runtime.lock().unwrap(); + // let runtime_settings; + let clash_state; + unsafe { + let runtime = runtime.0.as_ref().unwrap(); + // runtime_settings = runtime.settings_clone(); + clash_state = runtime.clash_state_clone(); + } + + let clash = match clash_state.write() { + Ok(x) => x, + Err(e) => { + log::error!("read clash_state failed to acquire state write lock: {}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + }; + + // let settings = match runtime_settings.write() { + // Ok(x) => x, + // Err(e) => { + // log::error!( + // "read runtime_settings failed to acquire state write lock: {}", + // e + // ); + // return Err(actix_web::Error::from(ClashError { + // Message: e.to_string(), + // ErrorKind: ClashErrorKind::InnerError, + // })); + // } + // }; + + // match clash.change_config( + // settings.skip_proxy, + // settings.override_dns, + // settings.allow_remote_access, + // settings.enhanced_mode, + // settings.dashboard.clone(), + // ) { + // Ok(_) => {} + // Err(e) => { + // log::error!("Failed while change clash config."); + // log::error!("Error Message:{}", e); + // return Err(actix_web::Error::from(ClashError { + // Message: e.to_string(), + // ErrorKind: ClashErrorKind::InnerError, + // })); + // } + // } + + match clash.restart_core().await { + Ok(_) => {} + Err(e) => { + log::error!("Failed while restart clash."); + log::error!("Error Message:{}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + } + + let r = GenLinkResponse { + message: "重启成功".to_string(), + status_code: 200, + }; + Ok(HttpResponse::Ok().json(r)) +} + +pub async fn reload_clash_config(state: web::Data) -> Result { + let runtime = state.runtime.lock().unwrap(); + let runtime_settings; + let clash_state; + unsafe { + let runtime = runtime.0.as_ref().unwrap(); + runtime_settings = runtime.settings_clone(); + clash_state = runtime.clash_state_clone(); + } + + let clash = match clash_state.write() { + Ok(x) => x, + Err(e) => { + log::error!("read clash_state failed: {}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + }; + + let settings = match runtime_settings.write() { + Ok(x) => x, + Err(e) => { + log::error!("read runtime_settings failed: {}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + }; + + match clash.change_config( + settings.skip_proxy, + settings.override_dns, + settings.allow_remote_access, + settings.enhanced_mode, + settings.dashboard.clone(), + ) { + Ok(_) => {} + Err(e) => { + log::error!("Failed while change clash config."); + log::error!("Error Message:{}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + } + + match clash.reload_config().await { + Ok(_) => {} + Err(e) => { + log::error!("Failed while reload clash config."); + log::error!("Error Message:{}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + } + + let r = GenLinkResponse { + message: "重载成功".to_string(), + status_code: 200, + }; + Ok(HttpResponse::Ok().json(r)) +} + pub async fn get_config(state: web::Data) -> Result { let runtime = state.runtime.lock().unwrap(); let runtime_settings; + let clash_state; unsafe { let runtime = runtime.0.as_ref().unwrap(); runtime_settings = runtime.settings_clone(); + clash_state = runtime.clash_state_clone(); } + + let clash = match clash_state.read() { + Ok(x) => x, + Err(e) => { + log::error!("read clash_state failed: {}", e); + return Err(actix_web::Error::from(ClashError { + Message: e.to_string(), + ErrorKind: ClashErrorKind::InnerError, + })); + } + }; + match runtime_settings.read() { Ok(x) => { + let secret = match clash.get_running_secret() { + Ok(s) => s, + Err(_) => x.secret.clone(), + }; + let r = GetConfigResponse { skip_proxy: x.skip_proxy, override_dns: x.override_dns, + allow_remote_access: x.allow_remote_access, enhanced_mode: x.enhanced_mode, + dashboard: x.dashboard.clone(), + secret: secret, status_code: 200, }; return Ok(HttpResponse::Ok().json(r)); @@ -283,7 +566,7 @@ pub async fn download_sub( let home = match runtime_state.read() { Ok(state) => state.home.clone(), - Err(_) => State::default().home + Err(_) => State::default().home, }; let path: PathBuf = home.join(".config/tomoon/subs/"); @@ -292,10 +575,10 @@ pub async fn download_sub( let local_file = PathBuf::from(local_file); let filename = (|| -> Result { // 如果文件名可被读取则采用 - let mut filename = String::from( - local_file.file_name().ok_or(())? - .to_str().ok_or(())?); - if !filename.to_lowercase().ends_with(".yaml") && !filename.to_lowercase().ends_with(".yml") { + let mut filename = String::from(local_file.file_name().ok_or(())?.to_str().ok_or(())?); + if !filename.to_lowercase().ends_with(".yaml") + && !filename.to_lowercase().ends_with(".yml") + { filename += ".yaml"; } Ok(filename) @@ -304,10 +587,11 @@ pub async fn download_sub( log::warn!("The subscription does not have a proper file name."); // 否则采用随机名字 rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(5) - .map(char::from) - .collect::() + ".yaml" + .sample_iter(&Alphanumeric) + .take(5) + .map(char::from) + .collect::() + + ".yaml" }); if local_file.exists() { let file_content = match fs::read_to_string(local_file) { @@ -394,11 +678,11 @@ pub async fn download_sub( let base_url = "http://127.0.0.1:25500/sub"; let target = "clash"; let config = "http://127.0.0.1:55556/ACL4SSR_Online.ini"; - + // 对参数进行 URL 编码 let encoded_url = urlencoding::encode(url.as_str()); let encoded_config = urlencoding::encode(config); - + // 构建请求 URL url = format!( "{}?target={}&url={}&insert=false&config={}&emoji=true&list=false&tfo=false&scv=true&fdn=false&expand=true&sort=false&new_name=true", @@ -408,7 +692,10 @@ pub async fn download_sub( match minreq::get(url.clone()) .with_header( "User-Agent", - format!("ToMoon/{} mihomo/1.18.3 Clash/v1.18.0", env!("CARGO_PKG_VERSION")), + format!( + "ToMoon/{} mihomo/1.18.3 Clash/v1.18.0", + env!("CARGO_PKG_VERSION") + ), ) .with_timeout(120) .send() @@ -426,15 +713,20 @@ pub async fn download_sub( let filename = x.headers.get("content-disposition"); let filename = match filename { Some(x) => { - let filename = x - .split("filename=").collect::>()[1] - .split(";").collect::>()[0] + let filename = x.split("filename=").collect::>()[1] + .split(";") + .collect::>()[0] .replace("\"", ""); filename.to_string() } None => { let slash_split = *url.split("/").collect::>().last().unwrap(); - slash_split.split("?").collect::>().first().unwrap().to_string() + slash_split + .split("?") + .collect::>() + .first() + .unwrap() + .to_string() } }; let filename = if filename.is_empty() { @@ -443,7 +735,9 @@ pub async fn download_sub( } else { filename }; - let filename = if filename.to_lowercase().ends_with(".yaml") || filename.to_lowercase().ends_with(".yml") { + let filename = if filename.to_lowercase().ends_with(".yaml") + || filename.to_lowercase().ends_with(".yml") + { filename } else { filename + ".yaml" diff --git a/backend/src/main.rs b/backend/src/main.rs index 5a6adae..2361498 100755 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -66,7 +66,9 @@ async fn main() -> Result<(), std::io::Error> { }); // 启动一个 tokio 任务来运行 subconverter - let subconverter_path = helper::get_current_working_dir().unwrap().join("bin/subconverter"); + let subconverter_path = helper::get_current_working_dir() + .unwrap() + .join("bin/subconverter"); tokio::spawn(async move { if subconverter_path.exists() && subconverter_path.is_file() { let mut command = tokio::process::Command::new(subconverter_path); @@ -89,7 +91,10 @@ async fn main() -> Result<(), std::io::Error> { Err(e) => log::error!("Failed to start subconverter: {}", e), } } else { - log::error!("Subconverter path does not exist or is not a file: {:?}", subconverter_path); + log::error!( + "Subconverter path does not exist or is not a file: {:?}", + subconverter_path + ); } }); @@ -116,10 +121,33 @@ async fn main() -> Result<(), std::io::Error> { .route(web::get().to(external_web::get_local_web_address)), ) .service(web::resource("/skip_proxy").route(web::post().to(external_web::skip_proxy))) - .service(web::resource("/override_dns").route(web::post().to(external_web::override_dns))) - .service(web::resource("/enhanced_mode").route(web::post().to(external_web::enhanced_mode))) + .service( + web::resource("/override_dns").route(web::post().to(external_web::override_dns)), + ) + .service( + web::resource("/enhanced_mode").route(web::post().to(external_web::enhanced_mode)), + ) .service(web::resource("/get_config").route(web::get().to(external_web::get_config))) //.service(web::resource("/manual").route(web::get().to(external_web.web_download_sub))) + // allow_remote_access + .service( + web::resource("/allow_remote_access") + .route(web::post().to(external_web::allow_remote_access)), + ) + // reload_clash_config + .service( + web::resource("/reload_clash_config") + .route(web::get().to(external_web::reload_clash_config)), + ) + // restart_clash + .service( + web::resource("/restart_clash").route(web::get().to(external_web::restart_clash)), + ) + // set_dashboard + .service( + web::resource("/set_dashboard").route(web::post().to(external_web::set_dashboard)), + ) + // web .service( fs::Files::new("/", "./web") .index_file("index.html") diff --git a/backend/src/settings.rs b/backend/src/settings.rs index 769ea32..8ff16d7 100755 --- a/backend/src/settings.rs +++ b/backend/src/settings.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, fmt::Display}; +use std::{fmt::Display, path::PathBuf}; use crate::helper; @@ -19,6 +19,12 @@ pub struct Settings { pub current_sub: String, #[serde(default = "default_subscriptions")] pub subscriptions: Vec, + #[serde(default = "default_allow_remote_access")] + pub allow_remote_access: bool, + #[serde(default = "default_dashboard")] + pub dashboard: String, + #[serde(default = "default_secret")] + pub secret: String, } fn default_skip_proxy() -> bool { @@ -33,12 +39,26 @@ fn default_override_dns() -> bool { true } +fn default_allow_remote_access() -> bool { + false +} + fn default_enhanced_mode() -> EnhancedMode { EnhancedMode::FakeIp } +fn default_dashboard() -> String { + "yacd-meta".to_string() +} + +fn default_secret() -> String { + "".to_string() +} + fn default_current_sub() -> String { - let default_profile = helper::get_current_working_dir().unwrap().join("bin/core/config.yaml"); + let default_profile = helper::get_current_working_dir() + .unwrap() + .join("bin/core/config.yaml"); default_profile.to_string_lossy().to_string() } @@ -48,8 +68,8 @@ fn default_subscriptions() -> Vec { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Subscription { - pub path : String, - pub url : String + pub path: String, + pub url: String, } #[derive(Debug)] @@ -68,13 +88,14 @@ impl Display for JsonError { } impl Subscription { - pub fn new(path: String, url: String) -> Self - { - Self { path: path, url: url } + pub fn new(path: String, url: String) -> Self { + Self { + path: path, + url: url, + } } } - #[derive(Debug)] pub struct State { pub home: PathBuf, @@ -86,9 +107,9 @@ impl State { let def = Self::default(); if cfg!(debug_assertions) { return Self { - home: "./tmp".into(), - dirty: true, - } + home: "./tmp".into(), + dirty: true, + }; } Self { home: usdpl_back::api::dirs::home().unwrap_or(def.home), @@ -106,7 +127,6 @@ impl Default for State { } } - impl Settings { pub fn save>(&self, path: P) -> Result<(), JsonError> { let path = path.as_ref(); @@ -125,14 +145,19 @@ impl Settings { impl Default for Settings { fn default() -> Self { - let default_profile = helper::get_current_working_dir().unwrap().join("bin/core/config.yaml"); + let default_profile = helper::get_current_working_dir() + .unwrap() + .join("bin/core/config.yaml"); Self { enable: false, skip_proxy: true, override_dns: true, enhanced_mode: EnhancedMode::FakeIp, current_sub: default_profile.to_string_lossy().to_string(), - subscriptions: Vec::new() + subscriptions: Vec::new(), + allow_remote_access: default_allow_remote_access(), + dashboard: default_dashboard(), + secret: default_secret(), } } -} \ No newline at end of file +} diff --git a/backend/src/test.rs b/backend/src/test.rs index 1f86ee8..c5a6aa1 100755 --- a/backend/src/test.rs +++ b/backend/src/test.rs @@ -72,7 +72,7 @@ mod tests { fn test_yaml() { println!("{}", std::env::current_dir().unwrap().to_str().unwrap()); let mut clash = control::Clash::default(); - clash.change_config(true, true); + clash.change_config(true, true, true, true); } #[test] diff --git a/main.py b/main.py index dc6926a..8007c30 100644 --- a/main.py +++ b/main.py @@ -1,27 +1,39 @@ -import subprocess import asyncio import os -from config import logger, setup_logger -import update +import subprocess + +import dashboard import decky +import update import utils +from config import logger, setup_logger +from settings import SettingsManager + class Plugin: backend_proc = None + # Asyncio-compatible long-running code, executed in a task when the plugin is loaded async def _main(self): logger = setup_logger() + self.settings = SettingsManager( + name="config", settings_directory=decky.DECKY_PLUGIN_SETTINGS_DIR + ) + utils.write_font_config() + dashboard_list = dashboard.get_dashboard_list() + logger.info(f"dashboard_list: {dashboard_list}") + logger.info("Start Tomoon.") - os.system('chmod -R a+x ' + decky.DECKY_PLUGIN_DIR) + os.system("chmod -R a+x " + decky.DECKY_PLUGIN_DIR) # 切换到工作目录 os.chdir(decky.DECKY_PLUGIN_DIR) self.backend_proc = subprocess.Popen([decky.DECKY_PLUGIN_DIR + "/bin/tomoon"]) while True: await asyncio.sleep(1) - + # Function called first during the unload process, utilize this to handle your plugin being removed async def _unload(self): logger.info("Stop Tomoon.") @@ -29,6 +41,22 @@ async def _unload(self): utils.remove_font_config() pass + async def get_settings(self): + return self.settings.getSetting(CONFIG_KEY) + + async def set_settings(self, settings): + self.settings.setSetting(CONFIG_KEY, settings) + logger.info(f"save Settings: {settings}") + return True + + async def get_config_value(self, key): + return self.settings.getSetting(key) + + async def set_config_value(self, key, value): + self.settings.setSetting(key, value) + logger.info(f"save config: {key} : {value}") + return True + async def update_latest(self): logger.info("Updating latest") return update.update_latest() @@ -41,4 +69,7 @@ async def get_version(self): async def get_latest_version(self): version = update.get_latest_version() logger.info(f"Latest version: {version}") - return version \ No newline at end of file + return version + + async def get_dashboard_list(self): + return dashboard.get_dashboard_list() diff --git a/py_modules/config.py b/py_modules/config.py index e84d964..fff1c4a 100644 --- a/py_modules/config.py +++ b/py_modules/config.py @@ -1,5 +1,6 @@ import logging + def setup_logger(): logging.basicConfig( level=logging.INFO, @@ -10,9 +11,12 @@ def setup_logger(): ) return logging.getLogger() + logger = setup_logger() # can be changed to logging.DEBUG for debugging issues logger.setLevel(logging.INFO) API_URL = "https://api.github.com/repos/YukiCoco/ToMoon/releases/latest" + +CONFIG_KEY = "tomoon" diff --git a/py_modules/dashboard.py b/py_modules/dashboard.py new file mode 100644 index 0000000..0d0b836 --- /dev/null +++ b/py_modules/dashboard.py @@ -0,0 +1,39 @@ +import os +from pathlib import Path + +import decky_plugin +from config import logger + +defalut_dashboard = os.path.join( + decky_plugin.DECKY_PLUGIN_DIR, "bin", "core", "dashboard", "yacd-meta" +) +defalut_dashboard_list = [defalut_dashboard] + + +def get_dashboard_list(): + dashboard_list = [] + + try: + # 遍历 dashboard_dir 下深度 1 的路径, 如果存在 xxx/index.html 则认为是一个 dashboard + dashboard_dir_path = Path(f"{decky_plugin.DECKY_PLUGIN_DIR}/bin/core/web") + + for path in dashboard_dir_path.iterdir(): + if path.is_dir() and (path / "index.html").exists(): + dashboard_list.append(str(path)) + + custom_dashboard_dir_path = Path( + f"{decky_plugin.DECKY_PLUGIN_SETTINGS_DIR}/dashboard" + ) + # 如果 custom_dashboard_dir_path 不存在 创建 + if not custom_dashboard_dir_path.is_dir(): + custom_dashboard_dir_path.mkdir(parents=True) + for path in custom_dashboard_dir_path.iterdir(): + if path.is_dir() and (path / "index.html").exists(): + dashboard_list.append(str(path)) + + logger.info(f"get_dashboard_list: {dashboard_list}") + + return dashboard_list + except Exception as e: + logger.error(f"error during get_dashboard_list: {e}") + return defalut_dashboard_list diff --git a/src/backend.ts b/src/backend.ts deleted file mode 100644 index 33bdbba..0000000 --- a/src/backend.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { call } from "@decky/api"; -import { init_usdpl, init_embedded, call_backend } from "usdpl-front"; - -const USDPL_PORT: number = 55555; - -// Utility - -export function resolve(promise: Promise, setter: any) { - (async function () { - let data = await promise; - if (data != null) { - console.debug("Got resolved", data); - setter(data); - } else { - console.warn("Resolve failed:", data); - } - })(); -} - -export function execute(promise: Promise) { - (async function () { - let data = await promise; - console.debug("Got executed", data); - })(); -} - -export async function initBackend() { - // init usdpl - await init_embedded(); - init_usdpl(USDPL_PORT); - //setReady(true); -} - -// Back-end functions - -export async function setEnabled(value: boolean): Promise { - return (await call_backend("set_clash_status", [value]))[0]; -} - -export async function getEnabled(): Promise { - return (await call_backend("get_clash_status", []))[0]; -} - -export async function resetNetwork(): Promise { - return await call_backend("reset_network", []); -} - -export async function downloadSub(value: String): Promise { - return (await call_backend("download_sub", [value]))[0]; -} - -export async function getDownloadStatus(): Promise { - return (await call_backend("get_download_status", []))[0]; -} - -export async function getSubList(): Promise { - return (await call_backend("get_sub_list", []))[0]; -} - -export async function deleteSub(value: Number): Promise { - return (await call_backend("delete_sub", [value]))[0]; -} - -export async function setSub(value: String): Promise { - return (await call_backend("set_sub", [value]))[0]; -} - -export async function updateSubs(): Promise { - return (await call_backend("update_subs", []))[0]; -} - -export async function getUpdateStatus(): Promise { - return (await call_backend("get_update_status", []))[0]; -} - -export async function createDebugLog(): Promise { - return (await call_backend("create_debug_log", []))[0]; -} - -export async function getRunningStatus(): Promise { - return (await call_backend("get_running_status", []))[0]; -} - -export async function getCurrentSub(): Promise { - return (await call_backend("get_current_sub", []))[0]; -} - -export class PyBackendData { - private current_version = ""; - private latest_version = ""; - - public async init() { - const version = await call("get_version") as string || ""; - if (version) { - this.current_version = version; - } - - const latest_version = await call("get_latest_version") as string || ""; - if (latest_version) { - this.latest_version = latest_version; - } - } - - public getCurrentVersion() { - return this.current_version; - } - - public setCurrentVersion(version: string) { - this.current_version = version; - } - - public getLatestVersion() { - return this.latest_version; - } - - public setLatestVersion(version: string) { - this.latest_version = version; - } -} - -export class PyBackend { - public static data: PyBackendData; - - public static async init() { - this.data = new PyBackendData(); - this.data.init(); - } - - public static async getLatestVersion(): Promise { - const version = await call("get_latest_version") as string || ""; - - const versionReg = /^\d+\.\d+\.\d+$/; - if (!versionReg.test(version)) { - return ""; - } - return version; - } - - // updateLatest - public static async updateLatest() { - await call("update_latest"); - } - - // get_version - public static async getVersion() { - return (await call("get_version")) as string; - } -} diff --git a/src/backend/backend.ts b/src/backend/backend.ts new file mode 100644 index 0000000..4eebb91 --- /dev/null +++ b/src/backend/backend.ts @@ -0,0 +1,284 @@ +import { call } from "@decky/api"; +import { init_usdpl, init_embedded, call_backend } from "usdpl-front"; +import axios from "axios"; +import { EnhancedMode } from "."; + +const USDPL_PORT: number = 55555; + +// Utility + +export function resolve(promise: Promise, setter: any) { + (async function () { + let data = await promise; + if (data != null) { + console.debug("Got resolved", data); + setter(data); + } else { + console.warn("Resolve failed:", data); + } + })(); +} + +export function execute(promise: Promise) { + (async function () { + let data = await promise; + console.debug("Got executed", data); + })(); +} + +export async function initBackend() { + // init usdpl + await init_embedded(); + init_usdpl(USDPL_PORT); + //setReady(true); +} + +// Back-end functions + +export async function setEnabled(value: boolean): Promise { + return (await call_backend("set_clash_status", [value]))[0]; +} + +export async function getEnabled(): Promise { + return (await call_backend("get_clash_status", []))[0]; +} + +export async function resetNetwork(): Promise { + return await call_backend("reset_network", []); +} + +export async function downloadSub(value: String): Promise { + return (await call_backend("download_sub", [value]))[0]; +} + +export async function getDownloadStatus(): Promise { + return (await call_backend("get_download_status", []))[0]; +} + +export async function getSubList(): Promise { + return (await call_backend("get_sub_list", []))[0]; +} + +export async function deleteSub(value: Number): Promise { + return (await call_backend("delete_sub", [value]))[0]; +} + +export async function setSub(value: String): Promise { + return (await call_backend("set_sub", [value]))[0]; +} + +export async function updateSubs(): Promise { + return (await call_backend("update_subs", []))[0]; +} + +export async function getUpdateStatus(): Promise { + return (await call_backend("get_update_status", []))[0]; +} + +export async function createDebugLog(): Promise { + return (await call_backend("create_debug_log", []))[0]; +} + +export async function getRunningStatus(): Promise { + return (await call_backend("get_running_status", []))[0]; +} + +export async function getCurrentSub(): Promise { + return (await call_backend("get_current_sub", []))[0]; +} + +export class PyBackendData { + private current_version = ""; + private latest_version = ""; + + public async init() { + const version = ((await call("get_version")) as string) || ""; + if (version) { + this.current_version = version; + } + + const latest_version = ((await call("get_latest_version")) as string) || ""; + if (latest_version) { + this.latest_version = latest_version; + } + } + + public getCurrentVersion() { + return this.current_version; + } + + public setCurrentVersion(version: string) { + this.current_version = version; + } + + public getLatestVersion() { + return this.latest_version; + } + + public setLatestVersion(version: string) { + this.latest_version = version; + } +} + +export class PyBackend { + public static data: PyBackendData; + + public static async init() { + this.data = new PyBackendData(); + this.data.init(); + } + + public static async getLatestVersion(): Promise { + const version = ((await call("get_latest_version")) as string) || ""; + + const versionReg = /^\d+\.\d+\.\d+$/; + if (!versionReg.test(version)) { + return ""; + } + return version; + } + + // updateLatest + public static async updateLatest() { + // await this.serverAPI!.callPluginMethod("update_latest", {}); + await call("update_latest", []); + } + + // get_version + public static async getVersion() { + // return (await this.serverAPI!.callPluginMethod("get_version", {})) + // .result as string; + return (await call("get_version", [])) as string; + } + + // get_dashboard_list + public static async getDashboardList() { + return (await call("get_dashboard_list")) as string[]; + } + + // get_config_value + public static async getConfigValue(key: string) { + return await call<[key: string], string | undefined>( + "get_config_value", + key + ); + } + + // set_config_value + public static async setConfigValue(key: string, value: string) { + return await call<[key: string, value: string], boolean>( + "set_config_value", + key, + value + ); + } + + private static async getDefalutDashboard() { + const dashboardList = await this.getDashboardList(); + return ( + dashboardList.find((x) => + (x.split("/").pop() || "").includes("yacd-meta") + ) || dashboardList[0] + ); + } + + public static async getCurrentDashboard() { + return ( + (await this.getConfigValue("current_dashboard")) || + (await this.getDefalutDashboard()) + ); + } + + public static async setCurrentDashboard(dashboard: string) { + return await this.setConfigValue("current_dashboard", dashboard); + } +} + +export enum ApiCallMethod { + GET = "GET", + POST = "POST", +} + +/** + * 调用后端 API 的通用方法 + * @param name API 端点名称 + * @param params 请求参数 + * @param method 请求方法,默认为 POST + * @returns Promise API 调用的响应 + */ +export function apiCallMethod( + name: string, + params: {}, + method: ApiCallMethod = ApiCallMethod.POST +): Promise { + const url = `http://localhost:55556/${name}`; + const headers = { "content-type": "application/x-www-form-urlencoded" }; + + // 封装请求逻辑,根据 method 参数决定使用 GET 还是 POST + const makeRequest = () => { + if (method === ApiCallMethod.GET) { + return axios.get(url, { headers: headers }); + } else { + return axios.post(url, params, { headers: headers }); + } + }; + + const ignore_list = ["get_config", "reload_clash_config", "restart_clash"]; + + // 在请求完成后自动触发配置重载 + if (!ignore_list.includes(name)) { + return makeRequest().then(async (response) => { + // 在原始请求完成后触发配置重载 + if (response.status === 200) { + console.log(`^^^^^^^^^^^ apiCallMethod: ${name} done`); + apiCallMethod("reload_clash_config", {}, ApiCallMethod.GET); + } + return response; // 返回原始请求的响应 + }); + } + + // 如果是重载配置请求,直接执行并返回结果 + return makeRequest(); +} + +export class ApiCallBackend { + public static async getConfig() { + return await apiCallMethod("get_config", {}, ApiCallMethod.GET); + } + + public static async reloadClashConfig() { + return await apiCallMethod("reload_clash_config", {}, ApiCallMethod.GET); + } + + // restart_clash + public static async restartClash() { + return await apiCallMethod("restart_clash", {}, ApiCallMethod.GET); + } + + // enhanced_mode + public static async enhancedMode(value: EnhancedMode) { + return await apiCallMethod("enhanced_mode", { enhanced_mode: value }); + } + + // override_dns + public static async overrideDns(value: boolean) { + return await apiCallMethod("override_dns", { override_dns: value }); + } + + // skip_proxy + public static async skipProxy(value: boolean) { + return await apiCallMethod("skip_proxy", { skip_proxy: value }); + } + + // allow_remote_access + public static async allowRemoteAccess(value: boolean) { + return await apiCallMethod("allow_remote_access", { + allow_remote_access: value, + }); + } + + // set_dashboard + public static async setDashboard(value: String) { + return await apiCallMethod("set_dashboard", { dashboard: value }); + } +} diff --git a/src/backend/enum.ts b/src/backend/enum.ts new file mode 100644 index 0000000..ead7aa5 --- /dev/null +++ b/src/backend/enum.ts @@ -0,0 +1,4 @@ +export enum EnhancedMode { + RedirHost = "RedirHost", + FakeIp = "FakeIp", +} diff --git a/src/backend/index.ts b/src/backend/index.ts new file mode 100644 index 0000000..bbc4d2e --- /dev/null +++ b/src/backend/index.ts @@ -0,0 +1,2 @@ +export * from "./enum"; +export * from "./backend"; \ No newline at end of file diff --git a/src/components/SubList.tsx b/src/components/SubList.tsx new file mode 100644 index 0000000..a294cf7 --- /dev/null +++ b/src/components/SubList.tsx @@ -0,0 +1,37 @@ +import { ButtonItem } from "@decky/ui"; +import { FC } from "react"; +import * as backend from "../backend/backend"; +interface appProp { + Subscriptions: Array; + UpdateSub: any; + Refresh: Function; +} + +export const SubList: FC = ({ Subscriptions, UpdateSub, Refresh }) => { + return ( +

+ {Subscriptions.map((x) => { + return ( +
+ { + //删除订阅 + UpdateSub((source: Array) => { + let i = source.indexOf(x); + source.splice(i, 1); + return source; + }); + backend.resolve(backend.deleteSub(x.id), () => {}); + Refresh(); + }} + > + Delete + +
+ ); + })} +
+ ); +}; diff --git a/src/pages/Version.tsx b/src/components/Version.tsx similarity index 92% rename from src/pages/Version.tsx rename to src/components/Version.tsx index 7d472ac..60ff1c2 100644 --- a/src/pages/Version.tsx +++ b/src/components/Version.tsx @@ -1,7 +1,7 @@ import { PanelSection, PanelSectionRow, Field } from "@decky/ui"; import { FC, useEffect, useState } from "react"; -import { PyBackend } from "../backend"; -import { ActionButtonItem } from "./components/actionButtonItem"; +import { PyBackend } from "../backend/backend"; +import { ActionButtonItem } from "."; export const VersionComponent: FC = () => { diff --git a/src/pages/components/actionButtonItem.tsx b/src/components/actionButtonItem.tsx similarity index 100% rename from src/pages/components/actionButtonItem.tsx rename to src/components/actionButtonItem.tsx diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..95cf080 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,3 @@ +export * from "./actionButtonItem"; +export * from "./Version"; +export * from "./SubList"; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 6871640..8bda344 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,20 +17,12 @@ import { routerHook } from "@decky/api"; import { FC, useEffect, useState } from "react"; import { GiEgyptianBird } from "react-icons/gi"; -import { - Subscriptions, - About, - Debug, - VersionComponent -} from "./pages"; +import { Subscriptions, About, Debug } from "./pages"; -import * as backend from "./backend"; -import axios from "axios"; +import * as backend from "./backend/backend"; -enum EnhancedMode { - RedirHost = 'RedirHost', - FakeIp = 'FakeIp', -} +import { ApiCallBackend, PyBackend, EnhancedMode } from "./backend"; +import { ActionButtonItem, VersionComponent } from "./components"; let enabledGlobal = false; let enabledSkipProxy = false; @@ -38,104 +30,152 @@ let enabledOverrideDNS = false; let usdplReady = false; let subs: any[]; let subs_option: any[]; -let current_sub = ''; +let current_sub = ""; let enhanced_mode = EnhancedMode.FakeIp; +let dashboard_list: string[]; +let current_dashboard = ""; +let allow_remote_access = false; +let _secret = ""; -const Content: FC<{ }> = ({ }) => { - +const Content: FC<{}> = ({ }) => { if (!usdplReady) { - return ( - - Init... - - ) + return Init...; } const [clashState, setClashState] = useState(enabledGlobal); backend.resolve(backend.getEnabled(), setClashState); - axios.get("http://127.0.0.1:55556/get_config").then(r => { - // json print r.data - console.log(`>>>>>>>>>>>>>>> get_config: ${JSON.stringify(r.data, null ,2)}`); - - if (r.data.status_code == 200) { - enabledSkipProxy = r.data.skip_proxy; - enabledOverrideDNS = r.data.override_dns; - enhanced_mode = r.data.enhanced_mode; - } - }) - //setInterval(refreshSubOptions, 2000); - console.log("status :" + clashState); const [options, setOptions] = useState(subs_option); - const [optionDropdownDisabled, setOptionDropdownDisabled] = useState(enabledGlobal); - const [openDashboardDisabled, setOpenDashboardDisabled] = useState(!enabledGlobal); + const [optionDropdownDisabled, setOptionDropdownDisabled] = + useState(enabledGlobal); + const [openDashboardDisabled, setOpenDashboardDisabled] = useState( + !enabledGlobal + ); const [isSelectionDisabled, setIsSelectionDisabled] = useState(false); const [SelectionTips, setSelectionTips] = useState("Run Clash in background"); const [skipProxyState, setSkipProxyState] = useState(enabledSkipProxy); const [overrideDNSState, setOverrideDNSState] = useState(enabledOverrideDNS); const [currentSub, setCurrentSub] = useState(current_sub); const [enhancedMode, setEnhancedMode] = useState(enhanced_mode); + const [dashboardList, setDashboardList] = useState(dashboard_list); + const [currentDashboard, setCurrentDashboard] = + useState(current_dashboard); + const [allowRemoteAccess, setAllowRemoteAccess] = + useState(allow_remote_access); + const [secret, setSecret] = useState(_secret); const update_subs = () => { backend.resolve(backend.getSubList(), (v: String) => { - console.log(`getSubList: ${v}`); + // console.log(`getSubList: ${v}`); let x: Array = JSON.parse(v.toString()); - let re = new RegExp("(?<=subs\/).+\.yaml$"); + let re = new RegExp("(?<=subs/).+.yaml$"); let i = 0; - subs = x.map(x => { + subs = x.map((x) => { let name = re.exec(x.path); return { id: i++, name: name![0], - url: x.url - } + url: x.url, + }; }); - let items = x.map(x => { + let items = x.map((x) => { let name = re.exec(x.path); return { label: name![0], - data: x.path - } + data: x.path, + }; }); subs_option = items; - setOptions(subs_option) + setOptions(subs_option); console.log("Subs ready"); setIsSelectionDisabled(i == 0); //console.log(sub); }); - } + }; + + const getConfig = async () => { + await ApiCallBackend.getConfig().then((res) => { + console.log( + `~~~~~~~~~~~~~~~~~~~ getConfig: ${JSON.stringify(res.data, null, 2)}` + ); + if (res.data.status_code == 200) { + enabledSkipProxy = res.data.skip_proxy; + enabledOverrideDNS = res.data.override_dns; + enhanced_mode = res.data.enhanced_mode; + allow_remote_access = res.data.allow_remote_access; + _secret = res.data.secret; + + setSkipProxyState(enabledSkipProxy); + setOverrideDNSState(enabledOverrideDNS); + setEnhancedMode(enhanced_mode); + setAllowRemoteAccess(allow_remote_access); + setSecret(_secret); + } + }); + }; useEffect(() => { const getCurrentSub = async () => { const sub = await backend.getCurrentSub(); setCurrentSub(sub); - } - getCurrentSub(); - update_subs(); + }; + + const getDashboardList = async () => { + // console.log(`>>>>>> getDashboardList`); + const list = await PyBackend.getDashboardList(); + console.log(`>>>>>> getDashboardList: ${list}`); + setDashboardList(list); + }; + + const getCurrentDashboard = async () => { + const dashboard = await PyBackend.getCurrentDashboard(); + setCurrentDashboard(dashboard); + }; + + const loadDate = async () => { + await getConfig(); + + getCurrentSub(); + getDashboardList(); + getCurrentDashboard(); + update_subs(); + }; + + loadDate(); }, []); useEffect(() => { current_sub = currentSub; }, [currentSub]); + useEffect(() => { + dashboard_list = dashboardList; + }, [dashboardList]); + + useEffect(() => { + current_dashboard = currentDashboard; + }, [currentDashboard]); + const enhancedModeOptions = [ - {mode:EnhancedMode.RedirHost, label: "Redir Host"}, - {mode:EnhancedMode.FakeIp, label:"Fake IP"}, + { mode: EnhancedMode.RedirHost, label: "Redir Host" }, + { mode: EnhancedMode.FakeIp, label: "Fake IP" }, ]; - const enhancedModeNotchLabels : NotchLabel[] = enhancedModeOptions.map((opt, i) => { - return { - notchIndex: i, - label: opt.label, - value: i, - }; - }); + const enhancedModeNotchLabels: NotchLabel[] = enhancedModeOptions.map( + (opt, i) => { + return { + notchIndex: i, + label: opt.label, + value: i, + }; + } + ); const convertEnhancedMode = (value: number) => { return enhancedModeOptions[value].mode; - } + }; const convertEnhancedModeValue = (value: EnhancedMode) => { return enhancedModeOptions.findIndex((opt) => opt.mode === value); - } + }; return (
@@ -156,17 +196,20 @@ const Content: FC<{ }> = ({ }) => { if (!clashState) { let check_running_handle = setInterval(() => { backend.resolve(backend.getRunningStatus(), (v: String) => { - console.log(v); + // console.log(v); switch (v) { case "Loading": setSelectionTips("Loading ..."); break; case "Failed": - setSelectionTips("Failed to start, please check /tmp/tomoon.log"); + setSelectionTips( + "Failed to start, please check /tmp/tomoon.log" + ); setClashState(false); break; case "Success": setSelectionTips("Clash is running."); + getConfig(); break; } if (v != "Loading") { @@ -185,7 +228,7 @@ const Content: FC<{ }> = ({ }) => { = ({ }) => { // setOptions(subs_option); }} onChange={(x) => { - backend.resolve(backend.setSub(x.data), () => { + const setSub = async () => { + await backend.setSub(x.data); + await ApiCallBackend.reloadClashConfig(); + }; + backend.resolve(setSub(), () => { setIsSelectionDisabled(false); }); }} @@ -204,8 +251,8 @@ const Content: FC<{ }> = ({ }) => { { - Router.CloseSideMenus() - Router.Navigate("/tomoon-config") + Router.CloseSideMenus(); + Router.Navigate("/tomoon-config"); }} > Manage Subscriptions @@ -215,30 +262,79 @@ const Content: FC<{ }> = ({ }) => { { - Router.CloseSideMenus() - Navigation.NavigateToExternalWeb("http://127.0.0.1:9090/ui") - //Router.NavigateToExternalWeb("http://127.0.0.1:9090/ui") + Router.CloseSideMenus(); + let param = ""; + let page = "setup"; + const currentDashboard_name = + currentDashboard.split("/").pop() || "yacd-meta"; + if (currentDashboard_name) { + param = `/${currentDashboard_name}/#`; + if (secret) { + // secret 不为空时,使用完整的参数,但是不同 dashboard 使用不同的 page + switch (currentDashboard_name) { + case "metacubexd": + case "zashboard": + page = "setup"; + break; + default: + page = "proxies"; + break; + } + param += `/${page}?hostname=127.0.0.1&port=9090&secret=${secret}`; + } else if (currentDashboard_name == "metacubexd") { + // 即使没有设置 secret,metacubexd 也会有奇怪的跳转问题,加上host和port + param += `/${page}?hostname=127.0.0.1&port=9090`; + } + } + Navigation.NavigateToExternalWeb( + "http://127.0.0.1:9090/ui" + param + ); }} disabled={openDashboardDisabled} > Open Dashboard + + { + return { + label: path.split("/").pop(), + data: path, + }; + })} + selectedOption={currentDashboard} + onChange={(val) => { + console.log(`>>>>>>>>>>>>>>>> selected dashboard: ${val.data}`); + current_dashboard = val.data; + PyBackend.setCurrentDashboard(val.data); + ApiCallBackend.setDashboard(val.data.split("/").pop()); + }} + /> + + + { + ApiCallBackend.allowRemoteAccess(value); + setAllowRemoteAccess(value); + }} + > + { - axios.post("http://127.0.0.1:55556/skip_proxy", { - skip_proxy: value - }, { - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - }); + ApiCallBackend.skipProxy(value); setSkipProxyState(value); }} - > - + > = ({ }) => { description="Force Clash to hijack DNS query" checked={overrideDNSState} onChange={(value: boolean) => { - axios.post("http://127.0.0.1:55556/override_dns", { - override_dns: value - }, { - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - }); + ApiCallBackend.overrideDns(value); setOverrideDNSState(value); }} - > - + > - {overrideDNSState && - { - const _enhancedMode = convertEnhancedMode(value); - setEnhancedMode(_enhancedMode); - axios.post("http://127.0.0.1:55556/enhanced_mode", { - enhanced_mode: _enhancedMode - }, { - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - }); + {overrideDNSState && ( + + { + const _enhancedMode = convertEnhancedMode(value); + setEnhancedMode(_enhancedMode); + ApiCallBackend.enhancedMode(_enhancedMode); + }} + /> + + )} + + { + ApiCallBackend.restartClash(); }} - /> - } + > + Restart Core + + @@ -308,18 +408,18 @@ const DeckyPluginRouterTest: FC = () => { { title: "Subscriptions", content: , - route: "/tomoon-config/subscriptions" + route: "/tomoon-config/subscriptions", }, { title: "About", content: , - route: "/tomoon-config/about" + route: "/tomoon-config/about", }, { title: "Debug", content: , - route: "/tomoon-config/debug" - } + route: "/tomoon-config/debug", + }, ]} /> ); @@ -334,16 +434,16 @@ export default definePlugin(() => { backend.resolve(backend.getEnabled(), (v: boolean) => { enabledGlobal = v; }); - axios.get("http://127.0.0.1:55556/get_config").then(r => { - if (r.data.status_code == 200) { - enabledSkipProxy = r.data.skip_proxy; - enabledOverrideDNS = r.data.override_dns; - enhanced_mode = r.data.enhanced_mode; + ApiCallBackend.getConfig().then((res) => { + if (res.data.status_code == 200) { + enabledSkipProxy = res.data.skip_proxy; + enabledOverrideDNS = res.data.override_dns; + enhanced_mode = res.data.enhanced_mode; + allow_remote_access = res.data.allow_remote_access; } }); })(); - routerHook.addRoute("/tomoon-config", DeckyPluginRouterTest); return { diff --git a/src/pages/About.tsx b/src/pages/About.tsx index 02900be..9a75c9f 100644 --- a/src/pages/About.tsx +++ b/src/pages/About.tsx @@ -1,69 +1,75 @@ import { FC } from "react"; import { ButtonItem, PanelSectionRow, Navigation } from "@decky/ui"; import { FiGithub } from "react-icons/fi"; -import { FaSteamSymbol } from "react-icons/fa" -import { TbBrandTelegram } from "react-icons/tb" +import { FaSteamSymbol } from "react-icons/fa"; +import { TbBrandTelegram } from "react-icons/tb"; export const About: FC = () => { - return ( - // The outermost div is to push the content down into the visible area - <> -

- To Moon -

- - A network toolbox for SteamOS. -
-
- - } - label="ToMoon" - onClick={() => { - Navigation.NavigateToExternalWeb("https://github.com/YukiCoco/ToMoon") - }} - > - GitHub Repo - - -

- Developer -

- - } - label="Sayo Kurisu" - onClick={() => { - Navigation.NavigateToExternalWeb("https://steamcommunity.com/profiles/76561198217352855/") - }} - > - Steam Profile - - -

- Support -

- - Join our Telegram group for support. -
-
- - } - label="@steamdecktalk" - onClick={() => { - Navigation.NavigateToExternalWeb("https://github.com/YukiCoco/StaticFilesCDN/blob/main/deck_tg_2.jpg?raw=true") - }} - > - Telegram Group - - - - ); -} \ No newline at end of file + return ( + // The outermost div is to push the content down into the visible area + <> +

+ To Moon +

+ + A network toolbox for SteamOS. +
+
+ + } + label="ToMoon" + onClick={() => { + Navigation.NavigateToExternalWeb( + "https://github.com/YukiCoco/ToMoon" + ); + }} + > + GitHub Repo + + +

+ Developer +

+ + } + label="Sayo Kurisu" + onClick={() => { + Navigation.NavigateToExternalWeb( + "https://steamcommunity.com/profiles/76561198217352855/" + ); + }} + > + Steam Profile + + +

+ Support +

+ + Join our Telegram group for support. +
+
+ + } + label="@steamdecktalk" + onClick={() => { + Navigation.NavigateToExternalWeb( + "https://github.com/YukiCoco/StaticFilesCDN/blob/main/deck_tg_2.jpg?raw=true" + ); + }} + > + Telegram Group + + + + ); +}; diff --git a/src/pages/Debug.tsx b/src/pages/Debug.tsx index 3872013..134cbce 100644 --- a/src/pages/Debug.tsx +++ b/src/pages/Debug.tsx @@ -1,25 +1,25 @@ import { FC } from "react"; import { ButtonItem, PanelSectionRow } from "@decky/ui"; -import { VscDebug } from "react-icons/vsc" +import { VscDebug } from "react-icons/vsc"; -import * as backend from "../backend"; +import * as backend from "../backend/backend"; export const Debug: FC = () => { - return ( - // The outermost div is to push the content down into the visible area - <> - - } - label="Debug" - onClick={() => { - backend.resolve(backend.createDebugLog(), () => {}); - }} - description="Debug Log is located at /tmp/tomoon.debug.log , please send it to the developer." - > - Generate Debug Log - - - - ); -} \ No newline at end of file + return ( + // The outermost div is to push the content down into the visible area + <> + + } + label="Debug" + onClick={() => { + backend.resolve(backend.createDebugLog(), () => {}); + }} + description="Debug Log is located at /tmp/tomoon.debug.log , please send it to the developer." + > + Generate Debug Log + + + + ); +}; diff --git a/src/pages/Subscriptions.tsx b/src/pages/Subscriptions.tsx index adc2c0a..7632503 100644 --- a/src/pages/Subscriptions.tsx +++ b/src/pages/Subscriptions.tsx @@ -1,115 +1,115 @@ import { PanelSectionRow, TextField, ButtonItem } from "@decky/ui"; import { useReducer, useState, FC } from "react"; import { cleanPadding } from "../style"; -import { SubList } from "./components/SubList"; -import { QRCodeCanvas } from 'qrcode.react'; +import { SubList } from "../components"; +import { QRCodeCanvas } from "qrcode.react"; -import * as backend from "../backend"; +import * as backend from "../backend/backend"; import axios from "axios"; interface SubProp { - Subscriptions: Array, + Subscriptions: Array; } export const Subscriptions: FC = ({ Subscriptions }) => { - const [text, setText] = useState(""); - const [downloadTips, setDownloadTips] = useState(""); - const [subscriptions, updateSubscriptions] = useState(Subscriptions); - const [downlaodBtnDisable, setDownlaodBtnDisable] = useState(false); - const [updateBtnDisable, setUpdateBtnDisable] = useState(false); - const [_, forceUpdate] = useReducer(x => x + 1, 0); - const [updateTips, setUpdateTips] = useState(""); - const [QRPageUrl, setQRPageUrl] = useState(""); + const [text, setText] = useState(""); + const [downloadTips, setDownloadTips] = useState(""); + const [subscriptions, updateSubscriptions] = useState(Subscriptions); + const [downlaodBtnDisable, setDownlaodBtnDisable] = useState(false); + const [updateBtnDisable, setUpdateBtnDisable] = useState(false); + const [_, forceUpdate] = useReducer((x) => x + 1, 0); + const [updateTips, setUpdateTips] = useState(""); + const [QRPageUrl, setQRPageUrl] = useState(""); - let checkStatusHandler: any; - let checkUpdateStatusHandler: any; + let checkStatusHandler: any; + let checkUpdateStatusHandler: any; - const refreshDownloadStatus = () => { - backend.resolve(backend.getDownloadStatus(), (v: any) => { - let response = v.toString(); - switch (response) { - case "Downloading": - setDownloadTips("Downloading..."); - break; - case "Error": - setDownloadTips("Download Error"); - break; - case "Failed": - setDownloadTips("Download Failed"); - break; - case "Success": - setDownloadTips("Download Succeeded"); - // 刷新 Subs - refreshSubs(); - break; - } - if (response != "Downloading") { - clearInterval(checkStatusHandler); - setDownlaodBtnDisable(false); - } - }); - } + const refreshDownloadStatus = () => { + backend.resolve(backend.getDownloadStatus(), (v: any) => { + let response = v.toString(); + switch (response) { + case "Downloading": + setDownloadTips("Downloading..."); + break; + case "Error": + setDownloadTips("Download Error"); + break; + case "Failed": + setDownloadTips("Download Failed"); + break; + case "Success": + setDownloadTips("Download Succeeded"); + // 刷新 Subs + refreshSubs(); + break; + } + if (response != "Downloading") { + clearInterval(checkStatusHandler); + setDownlaodBtnDisable(false); + } + }); + }; - const refreshUpdateStatus = () => { - backend.resolve(backend.getUpdateStatus(), (v: any) => { - let response = v.toString(); - switch (response) { - case "Downloading": - setDownloadTips("Downloading... Please wait"); - break; - case "Error": - setDownloadTips("Update Error"); - break; - case "Failed": - setDownloadTips("Update Failed"); - break; - case "Success": - setDownloadTips("Update Succeeded"); - // 刷新 Subs - refreshSubs(); - break; - } - if (response != "Downloading") { - clearInterval(checkUpdateStatusHandler); - setUpdateBtnDisable(false); - } - }); - } + const refreshUpdateStatus = () => { + backend.resolve(backend.getUpdateStatus(), (v: any) => { + let response = v.toString(); + switch (response) { + case "Downloading": + setDownloadTips("Downloading... Please wait"); + break; + case "Error": + setDownloadTips("Update Error"); + break; + case "Failed": + setDownloadTips("Update Failed"); + break; + case "Success": + setDownloadTips("Update Succeeded"); + // 刷新 Subs + refreshSubs(); + break; + } + if (response != "Downloading") { + clearInterval(checkUpdateStatusHandler); + setUpdateBtnDisable(false); + } + }); + }; - const refreshSubs = () => { - backend.resolve(backend.getSubList(), (v: String) => { - let x: Array = JSON.parse(v.toString()); - let re = new RegExp("(?<=subs\/).+\.yaml$"); - let i = 0; - let subs = x.map(x => { - let name = re.exec(x.path); - return { - id: i++, - name: name![0], - url: x.url - } - }); - console.log("Subs refresh"); - updateSubscriptions(subs); - //console.log(sub); - }); - } + const refreshSubs = () => { + backend.resolve(backend.getSubList(), (v: String) => { + let x: Array = JSON.parse(v.toString()); + let re = new RegExp("(?<=subs/).+.yaml$"); + let i = 0; + let subs = x.map((x) => { + let name = re.exec(x.path); + return { + id: i++, + name: name![0], + url: x.url, + }; + }); + console.log("Subs refresh"); + updateSubscriptions(subs); + //console.log(sub); + }); + }; - //获取 QR Page - axios.get("http://127.0.0.1:55556/get_ip_address").then(r => { - if (r.data.status_code == 200) { - setQRPageUrl(`http://${r.data.ip}:55556`) - } else { - setQRPageUrl('') - } - }) + //获取 QR Page + axios.get("http://127.0.0.1:55556/get_ip_address").then((r) => { + if (r.data.status_code == 200) { + setQRPageUrl(`http://${r.data.ip}:55556`); + } else { + setQRPageUrl(""); + } + }); - console.log("load Subs page"); + console.log("load Subs page"); - return ( - <> - - -
- -
-
- setText(e?.target.value)} - description={downloadTips} - /> -
- { - setDownlaodBtnDisable(true); - backend.resolve(backend.downloadSub(text), () => { - console.log("download sub: " + text); - }); - checkStatusHandler = setInterval(refreshDownloadStatus, 500); - }}> - Download - - { - setUpdateBtnDisable(true); - backend.resolve(backend.updateSubs(), () => { - console.log("update subs."); - }); - checkUpdateStatusHandler = setInterval(refreshUpdateStatus, 500); - }} disabled={updateBtnDisable}> - Update All - -
- - {/* { + + +
+ +
+
+ setText(e?.target.value)} + description={downloadTips} + /> +
+ { + setDownlaodBtnDisable(true); + backend.resolve(backend.downloadSub(text), () => { + console.log("download sub: " + text); + }); + checkStatusHandler = setInterval(refreshDownloadStatus, 500); + }} + > + Download + + { + setUpdateBtnDisable(true); + backend.resolve(backend.updateSubs(), () => { + console.log("update subs."); + }); + checkUpdateStatusHandler = setInterval(refreshUpdateStatus, 500); + }} + disabled={updateBtnDisable} + > + Update All + +
+ + {/* { subscriptions.map(x => { return (
@@ -167,8 +176,12 @@ export const Subscriptions: FC = ({ Subscriptions }) => { ); }) } */} - - - - ); + + + + ); }; diff --git a/src/pages/components/SubList.tsx b/src/pages/components/SubList.tsx deleted file mode 100644 index f0702a4..0000000 --- a/src/pages/components/SubList.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ButtonItem } from "@decky/ui"; -import { FC } from "react"; -import * as backend from "../../backend"; -interface appProp { - Subscriptions: Array, - UpdateSub: any, - Refresh: Function -} - - -export const SubList: FC = ({ Subscriptions, UpdateSub, Refresh }) => { - return ( -
- { - Subscriptions.map((x) => { - return ( -
- { - //删除订阅 - UpdateSub((source: Array) => { - let i = source.indexOf(x) - source.splice(i, 1) - return source - }); - backend.resolve(backend.deleteSub(x.id), () => { }); - Refresh() - } - }>Delete -
- ); - }) - } -
- ); -} - diff --git a/src/pages/index.ts b/src/pages/index.ts index 51d6e2c..7b93984 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,4 +1,3 @@ export * from "./Subscriptions"; export * from "./About"; -export * from "./Debug"; -export * from "./Version"; \ No newline at end of file +export * from "./Debug"; \ No newline at end of file