🎉 创世提交
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
5650
src-tauri/Cargo.lock
generated
Normal file
31
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "spark-store"
|
||||
version = "4.9.9"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "spark_store_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12.12", features = ["json"] }
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
tokio-macros = { version = "2.5.0" }
|
||||
lazy_static = "1.5.0"
|
||||
futures = "0.3.31"
|
||||
pinyin = "0.10.0"
|
||||
tauri-plugin-clipboard-manager = "2.2.0"
|
||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
11
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"clipboard-manager:allow-write-text"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
66
src-tauri/src/handlers/app.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::{handlers::server::get_json_server_url, models::app::{AppDetail, AppItem}, utils::{format_icon_url, UA}};
|
||||
|
||||
use super::category::get_all_apps;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_info(category: String, pkgname: String) -> Result<AppDetail, String> {
|
||||
use crate::models::app::AppDetail;
|
||||
use crate::utils::UA;
|
||||
|
||||
// 获取服务器URL
|
||||
let json_server_url = get_json_server_url();
|
||||
let url = format!("{}{}/{}/app.json", json_server_url, category, pkgname);
|
||||
|
||||
// 创建HTTP客户端并发送请求
|
||||
let response = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("User-Agent", UA)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("获取应用信息失败: {}", e))?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("读取响应内容失败: {}", e))?;
|
||||
|
||||
// 直接解析JSON响应为AppDetail结构体
|
||||
let mut app_info: AppDetail = serde_json::from_str(&response)
|
||||
.map_err(|e| format!("解析应用信息JSON失败: {}", e))?;
|
||||
|
||||
// 设置图标URL和下载次数
|
||||
app_info.category = Some(category.clone());
|
||||
app_info.icon = Some(format_icon_url(&category.clone(), &app_info.pkgname));
|
||||
app_info.download_times = Some(get_download_times(category, pkgname)
|
||||
.await
|
||||
.map_err(|e| format!("获取下载次数失败: {}", e))?);
|
||||
|
||||
Ok(app_info)
|
||||
}
|
||||
|
||||
pub async fn get_download_times(category: String, pkgname: String) -> Result<i32, String> {
|
||||
let json_server_url = get_json_server_url();
|
||||
let url = format!("{}{}/{}/download-times.txt", json_server_url, category, pkgname);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("User-Agent", UA)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let times = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(times)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn search_all_apps(query: String) -> Result<Vec<AppItem>, String> {
|
||||
let all_apps = get_all_apps().await?;
|
||||
Ok(crate::utils::search::search_apps(&all_apps, &query))
|
||||
}
|
||||
100
src-tauri/src/handlers/category.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use crate::{models::category::Category, utils::format_icon_url};
|
||||
use crate::models::app::AppItem;
|
||||
use crate::utils::UA;
|
||||
use super::server::get_json_server_url;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use lazy_static::lazy_static;
|
||||
use futures::future::join_all;
|
||||
|
||||
lazy_static! {
|
||||
static ref APPS_CACHE: Mutex<HashMap<String, Vec<AppItem>>> = Mutex::new(HashMap::new());
|
||||
static ref CATEGORIES_CACHE: Mutex<Option<Vec<Category>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_category_apps(category_id: String) -> Result<Vec<AppItem>, String> {
|
||||
// 尝试从缓存中获取数据
|
||||
if let Some(cached_apps) = APPS_CACHE.lock().unwrap().get(&category_id) {
|
||||
return Ok(cached_apps.clone());
|
||||
}
|
||||
|
||||
// 如果缓存中没有,从服务器获取数据
|
||||
let json_server_url = get_json_server_url();
|
||||
let url = format!("{}{}/applist.json", json_server_url, category_id);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("User-Agent", UA)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut apps: Vec<AppItem> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 为每个应用设置图标URL
|
||||
for app in &mut apps {
|
||||
app.icon = Some(format_icon_url(&category_id, &app.pkgname));
|
||||
app.category = Some(category_id.clone());
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
APPS_CACHE.lock().unwrap().insert(category_id.clone(), apps.clone());
|
||||
|
||||
Ok(apps)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_all_apps() -> Result<Vec<AppItem>, String> {
|
||||
let categories = get_all_categories().await?;
|
||||
let futures: Vec<_> = categories
|
||||
.iter()
|
||||
.map(|category| get_category_apps(category.id.clone()))
|
||||
.collect();
|
||||
|
||||
let results = join_all(futures).await;
|
||||
let mut all_apps = Vec::new();
|
||||
|
||||
for result in results {
|
||||
match result {
|
||||
Ok(apps) => all_apps.extend(apps),
|
||||
Err(e) => eprintln!("Error fetching apps for category: {}", e)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_apps)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_all_categories() -> Result<Vec<Category>, String> {
|
||||
// 尝试从缓存中获取数据
|
||||
if let Some(cached_categories) = CATEGORIES_CACHE.lock().unwrap().as_ref() {
|
||||
return Ok(cached_categories.clone());
|
||||
}
|
||||
|
||||
// 如果缓存中没有,从服务器获取数据
|
||||
let json_server_url = get_json_server_url();
|
||||
let url = format!("{}category.json", json_server_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("User-Agent", UA)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let categories: Vec<Category> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 更新缓存
|
||||
*CATEGORIES_CACHE.lock().unwrap() = Some(categories.clone());
|
||||
|
||||
Ok(categories)
|
||||
}
|
||||
36
src-tauri/src/handlers/home.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::handlers::server::get_img_server_url;
|
||||
use crate::models::home::HomeLink;
|
||||
use crate::utils::UA;
|
||||
use super::server::get_json_server_url;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_home_links() -> Result<Vec<HomeLink>, String> {
|
||||
let json_server_url = get_json_server_url();
|
||||
let img_server_url = get_img_server_url();
|
||||
let url = format!("{}home/homelinks.json", json_server_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("User-Agent", UA)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 获取响应内容的文本形式
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 将文本解析为 HomeLink 向量
|
||||
let mut links: Vec<HomeLink> = serde_json::from_str(&response_text)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 为每个 HomeLink 的 img_url 添加图片服务器前缀
|
||||
for link in &mut links {
|
||||
link.img_url = format!("{}{}", img_server_url, link.img_url);
|
||||
}
|
||||
|
||||
Ok(links)
|
||||
}
|
||||
4
src-tauri/src/handlers/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod category;
|
||||
pub mod server;
|
||||
pub mod app;
|
||||
pub mod home;
|
||||
29
src-tauri/src/handlers/server.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
#[tauri::command]
|
||||
pub fn get_target_arch_to_store() -> String {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
return "amd64-store".to_string();
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{
|
||||
return "arm64-store".to_string();
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "loongarch64")]
|
||||
{
|
||||
return "loong64-store".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_json_server_url() -> String {
|
||||
let arch = get_target_arch_to_store();
|
||||
return format!("https://cdn.d.store.deepinos.org.cn/{}/", arch);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_img_server_url() -> String {
|
||||
let arch = get_target_arch_to_store();
|
||||
return format!("https://spk-json.spark-app.store/{}/", arch);
|
||||
}
|
||||
24
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
mod models;
|
||||
mod handlers;
|
||||
mod utils;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
handlers::category::get_all_categories,
|
||||
handlers::category::get_category_apps,
|
||||
handlers::category::get_all_apps,
|
||||
handlers::server::get_target_arch_to_store,
|
||||
handlers::server::get_json_server_url,
|
||||
handlers::server::get_img_server_url,
|
||||
handlers::app::get_app_info,
|
||||
handlers::app::search_all_apps,
|
||||
handlers::home::get_home_links,
|
||||
utils::get_user_agent,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
spark_store_lib::run()
|
||||
}
|
||||
51
src-tauri/src/models/app.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AppItem {
|
||||
#[serde(rename = "More")]
|
||||
pub more: String,
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "Pkgname")]
|
||||
pub pkgname: String,
|
||||
#[serde(rename = "Tags")]
|
||||
pub tags: Option<String>,
|
||||
#[serde(rename = "Update")]
|
||||
pub update: String,
|
||||
pub icon: Option<String>,
|
||||
pub category: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AppDetail {
|
||||
#[serde(rename = "More")]
|
||||
pub more: String,
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "Pkgname")]
|
||||
pub pkgname: String,
|
||||
#[serde(rename = "Tags")]
|
||||
pub tags: String,
|
||||
#[serde(rename = "Update")]
|
||||
pub update: String,
|
||||
#[serde(rename = "Icon")]
|
||||
pub icon: Option<String>,
|
||||
#[serde(rename = "Category")]
|
||||
pub category: Option<String>,
|
||||
#[serde(rename = "Version")]
|
||||
pub version: String,
|
||||
#[serde(rename = "Filename")]
|
||||
pub filename: String,
|
||||
#[serde(rename = "Torrent_address")]
|
||||
pub torrent_address: String,
|
||||
#[serde(rename = "Author")]
|
||||
pub author: String,
|
||||
#[serde(rename = "Contributor")]
|
||||
pub contributor: String,
|
||||
#[serde(rename = "Website")]
|
||||
pub website: String,
|
||||
#[serde(rename = "Size")]
|
||||
pub size: String,
|
||||
#[serde(rename = "DownloadTimes")]
|
||||
pub download_times: Option<i32>,
|
||||
}
|
||||
8
src-tauri/src/models/category.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize,Clone,Debug)]
|
||||
pub struct Category {
|
||||
pub id: String,
|
||||
pub icon: String,
|
||||
pub name_zh_cn: String,
|
||||
}
|
||||
12
src-tauri/src/models/home.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct HomeLink {
|
||||
pub name: String,
|
||||
pub more: String,
|
||||
#[serde(rename = "imgUrl")]
|
||||
pub img_url: String,
|
||||
#[serde(rename = "type")]
|
||||
pub link_type: String,
|
||||
pub url: String,
|
||||
}
|
||||
3
src-tauri/src/models/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod category;
|
||||
pub mod app;
|
||||
pub mod home;
|
||||
14
src-tauri/src/utils/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use crate::handlers::server::get_img_server_url;
|
||||
|
||||
pub mod search;
|
||||
|
||||
pub const UA: &str = concat!("Spark-Store/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_user_agent() -> String {
|
||||
UA.into()
|
||||
}
|
||||
|
||||
pub fn format_icon_url(category: &str, pkgname: &str) -> String {
|
||||
format!("{}{}/{}/icon.png", get_img_server_url(), category, pkgname)
|
||||
}
|
||||
60
src-tauri/src/utils/search.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use pinyin::ToPinyin;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn match_text(text: &str, query: &str) -> bool {
|
||||
let text_lower = text.to_lowercase();
|
||||
let query_lower = query.to_lowercase();
|
||||
|
||||
// 直接匹配原文
|
||||
if text_lower.contains(&query_lower) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取文本的拼音并转换为小写
|
||||
let text_pinyin: String = text
|
||||
.to_pinyin()
|
||||
.filter_map(|p| p)
|
||||
.map(|p| p.plain().to_lowercase())
|
||||
.collect();
|
||||
|
||||
// 获取查询的拼音并转换为小写
|
||||
// 判断查询是否为纯英文
|
||||
let query_pinyin = if query.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
query.to_lowercase()
|
||||
} else {
|
||||
query
|
||||
.to_pinyin()
|
||||
.filter_map(|p| p)
|
||||
.map(|p| p.plain().to_lowercase())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 匹配拼音
|
||||
text_pinyin.contains(&query_pinyin)
|
||||
}
|
||||
|
||||
pub fn search_apps(apps: &[crate::models::app::AppItem], query: &str) -> Vec<crate::models::app::AppItem> {
|
||||
if query.is_empty() {
|
||||
return apps.to_vec();
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for app in apps {
|
||||
// 避免重复
|
||||
if seen.contains(&app.pkgname) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 匹配名称、包名和描述
|
||||
if match_text(&app.name, query) ||
|
||||
match_text(&app.pkgname, query) ||
|
||||
match_text(&app.more, query) {
|
||||
results.push(app.clone());
|
||||
seen.insert(app.pkgname.clone());
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
35
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "spark-store",
|
||||
"version": "0.1.0",
|
||||
"identifier": "store.spark-app.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "yarn build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "spark-store",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||