🎉 创世提交

This commit is contained in:
柚子
2025-01-22 01:48:07 +08:00
commit 5d3481b9ea
92 changed files with 11705 additions and 0 deletions

View 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))
}

View 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)
}

View 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)
}

View File

@@ -0,0 +1,4 @@
pub mod category;
pub mod server;
pub mod app;
pub mod home;

View 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
View 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
View 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()
}

View 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>,
}

View 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,
}

View 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,
}

View File

@@ -0,0 +1,3 @@
pub mod category;
pub mod app;
pub mod home;

View 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)
}

View 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
}