diff --git a/README.md b/README.md index 0822396d..f1472e44 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ## 功能 -- [x] 支持 **`原神`** 和 **`崩坏:星穹铁道`** 游戏抽卡记录。 +- [x] 支持 **`原神`**、**`崩坏:星穹铁道`** 和 **`绝区零`** 游戏抽卡记录。 - [x] 管理游戏的多个账号。 - [x] 获取游戏的抽卡链接。 - [x] 获取抽卡记录并保存到本地数据库文件。 @@ -67,8 +67,6 @@ ## 特别感谢 * [UIGF organization](https://uigf.org) -* [DGP-Studio/Snap.Hutao](https://github.com/DGP-Studio/Snap.Hutao) -* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat) * [vikiboss/gs-helper](https://github.com/vikiboss/gs-helper) ## 协议 @@ -76,7 +74,7 @@ > [!NOTE] > MIT OR Apache-2.0 **仅供个人学习交流使用。请勿用于任何商业或违法违规用途。** > -> 本软件不会收集任何用户数据。所产生的数据(包括但不限于使用数据、抽卡数据、账号信息等)均保存在用户本地。 +> 本软件不会向您索要任何关于 ©miHoYo 账户的账号密码信息,也不会收集任何用户数据。所产生的数据(包括但不限于使用数据、抽卡数据、UID 信息等)均保存在用户本地。 ### 部分资源文件 @@ -89,4 +87,5 @@ * [src/assets/images/Logo.png](src/assets/images/Logo.png) * [src/assets/images/genshin/*](src/assets/images/genshin) * [src/assets/images/starrail/*](src/assets/images/starrail) +* [src/assets/images/zzz/*](src/assets/images/zzz) * [src-tauri/icons/*](src-tauri/icons/) diff --git a/src-tauri/src/gacha/impl_genshin.rs b/src-tauri/src/gacha/impl_genshin.rs index 28905c0c..517775e1 100644 --- a/src-tauri/src/gacha/impl_genshin.rs +++ b/src-tauri/src/gacha/impl_genshin.rs @@ -7,6 +7,7 @@ use super::{ GameDataDirectoryFinder, }; use crate::error::Result; +use crate::storage::entity_account::AccountFacet; use async_trait::async_trait; use reqwest::Client as Reqwest; use serde::{Deserialize, Serialize}; @@ -114,7 +115,12 @@ impl GachaRecordFetcher for GenshinGacha { end_id: Option<&str>, ) -> Result>> { let response = fetch_gacha_records::( - reqwest, ENDPOINT, gacha_url, gacha_type, end_id, + reqwest, + &AccountFacet::Genshin, + ENDPOINT, + gacha_url, + gacha_type, + end_id, ) .await?; diff --git a/src-tauri/src/gacha/impl_starrail.rs b/src-tauri/src/gacha/impl_starrail.rs index 672745c4..40610646 100644 --- a/src-tauri/src/gacha/impl_starrail.rs +++ b/src-tauri/src/gacha/impl_starrail.rs @@ -7,6 +7,7 @@ use super::{ GameDataDirectoryFinder, }; use crate::error::Result; +use crate::storage::entity_account::AccountFacet; use async_trait::async_trait; use reqwest::Client as Reqwest; use serde::{Deserialize, Serialize}; @@ -116,7 +117,12 @@ impl GachaRecordFetcher for StarRailGacha { end_id: Option<&str>, ) -> Result>> { let response = fetch_gacha_records::( - reqwest, ENDPOINT, gacha_url, gacha_type, end_id, + reqwest, + &AccountFacet::StarRail, + ENDPOINT, + gacha_url, + gacha_type, + end_id, ) .await?; diff --git a/src-tauri/src/gacha/impl_zzz.rs b/src-tauri/src/gacha/impl_zzz.rs new file mode 100644 index 00000000..77ca2846 --- /dev/null +++ b/src-tauri/src/gacha/impl_zzz.rs @@ -0,0 +1,147 @@ +use super::utilities::{ + fetch_gacha_records, lookup_cognosphere_dir, lookup_gacha_urls_from_endpoint, lookup_mihoyo_dir, + lookup_path_line_from_keyword, lookup_valid_cache_data_dir, +}; +use super::{ + GachaRecord, GachaRecordFetcher, GachaRecordFetcherChannel, GachaUrl, GachaUrlFinder, + GameDataDirectoryFinder, +}; +use crate::error::Result; +use crate::storage::entity_account::AccountFacet; +use async_trait::async_trait; +use reqwest::Client as Reqwest; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::cmp::Ordering; +use std::path::{Path, PathBuf}; + +#[derive(Default, Deserialize)] +pub struct ZenlessZoneZeroGacha; + +/// Game Directory + +impl GameDataDirectoryFinder for ZenlessZoneZeroGacha { + fn find_game_data_directories(&self) -> Result> { + let cognosphere_dir = lookup_cognosphere_dir(); + let mihoyo_dir = lookup_mihoyo_dir(); + let mut directories = Vec::new(); + + // TODO: Untested + const INTERNATIONAL_PLAYER_LOG: &str = "Zenless Zone Zero/Player.log"; + const INTERNATIONAL_DIR_KEYWORD: &str = "/ZenlessZoneZero_Data/"; + + let mut player_log = cognosphere_dir.join(INTERNATIONAL_PLAYER_LOG); + if let Some(directory) = lookup_path_line_from_keyword(player_log, INTERNATIONAL_DIR_KEYWORD)? { + directories.push(directory); + } + + const CHINESE_PLAYER_LOG: &str = "绝区零/Player.log"; + const CHINESE_DIR_KEYWORD: &str = "/ZenlessZoneZero_Data/"; + + player_log = mihoyo_dir.join(CHINESE_PLAYER_LOG); + if let Some(directory) = lookup_path_line_from_keyword(player_log, CHINESE_DIR_KEYWORD)? { + directories.push(directory); + } + + Ok(directories) + } +} + +/// Gacha Url + +const ENDPOINT: &str = "/api/getGachaLog?"; + +impl GachaUrlFinder for ZenlessZoneZeroGacha { + fn find_gacha_urls>(&self, game_data_dir: P) -> Result> { + // See: https://github.com/lgou2w/HoYo.Gacha/issues/10 + let cache_data_dir = lookup_valid_cache_data_dir(game_data_dir)?; + lookup_gacha_urls_from_endpoint(cache_data_dir, ENDPOINT, true) + } +} + +/// Gacha Record + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct ZenlessZoneZeroGachaRecord { + pub id: String, + pub uid: String, + pub gacha_id: String, + pub gacha_type: String, + pub item_id: String, + pub count: String, + pub time: String, + pub name: String, + pub lang: String, + pub item_type: String, + pub rank_type: String, +} + +impl GachaRecord for ZenlessZoneZeroGachaRecord { + fn id(&self) -> &str { + &self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl PartialOrd for ZenlessZoneZeroGachaRecord { + fn partial_cmp(&self, other: &Self) -> Option { + self.id.partial_cmp(&other.id) + } +} + +/// Gacha Record Fetcher + +#[allow(unused)] +#[derive(Deserialize)] +pub(crate) struct ZenlessZoneZeroGachaRecordPagination { + page: String, + size: String, + // total: String, + list: Vec, + region: String, + region_time_zone: i8, +} + +#[async_trait] +impl GachaRecordFetcher for ZenlessZoneZeroGacha { + type Target = ZenlessZoneZeroGachaRecord; + + async fn fetch_gacha_records( + &self, + reqwest: &Reqwest, + gacha_url: &str, + gacha_type: Option<&str>, + end_id: Option<&str>, + ) -> Result>> { + let response = fetch_gacha_records::( + reqwest, + &AccountFacet::ZenlessZoneZero, + ENDPOINT, + gacha_url, + gacha_type, + end_id, + ) + .await?; + + Ok(response.data.map(|pagination| pagination.list)) + } + + async fn fetch_gacha_records_any_uid( + &self, + reqwest: &Reqwest, + gacha_url: &str, + ) -> Result> { + let result = self + .fetch_gacha_records(reqwest, gacha_url, None, None) + .await?; + Ok(result.and_then(|gacha_records| gacha_records.first().map(|record| record.uid.clone()))) + } +} + +#[async_trait] +impl GachaRecordFetcherChannel for ZenlessZoneZeroGacha { + type Fetcher = Self; +} diff --git a/src-tauri/src/gacha/mod.rs b/src-tauri/src/gacha/mod.rs index 1325a11b..72527f6b 100644 --- a/src-tauri/src/gacha/mod.rs +++ b/src-tauri/src/gacha/mod.rs @@ -1,6 +1,7 @@ mod declare; mod impl_genshin; mod impl_starrail; +mod impl_zzz; mod plugin; mod utilities; @@ -10,4 +11,5 @@ pub mod uigf; pub use declare::*; pub use impl_genshin::*; pub use impl_starrail::*; +pub use impl_zzz::*; pub use plugin::*; diff --git a/src-tauri/src/gacha/plugin.rs b/src-tauri/src/gacha/plugin.rs index 1fce9c85..f91aeebc 100644 --- a/src-tauri/src/gacha/plugin.rs +++ b/src-tauri/src/gacha/plugin.rs @@ -1,6 +1,7 @@ use super::srgf; use super::uigf; use super::utilities::{create_default_reqwest, find_gacha_url_and_validate_consistency}; +use super::ZenlessZoneZeroGacha; use super::{ create_fetcher_channel, GachaRecordFetcherChannelFragment, GachaUrlFinder, GameDataDirectoryFinder, GenshinGacha, StarRailGacha, @@ -23,6 +24,7 @@ async fn find_game_data_directories(facet: AccountFacet) -> Result> match facet { AccountFacet::Genshin => GenshinGacha.find_game_data_directories(), AccountFacet::StarRail => StarRailGacha.find_game_data_directories(), + AccountFacet::ZenlessZoneZero => ZenlessZoneZeroGacha.find_game_data_directories(), } } @@ -41,6 +43,11 @@ async fn find_gacha_url( let gacha_urls = StarRailGacha.find_gacha_urls(game_data_dir)?; find_gacha_url_and_validate_consistency(&StarRailGacha, &facet, &uid, &gacha_urls).await? } + AccountFacet::ZenlessZoneZero => { + let gacha_urls = ZenlessZoneZeroGacha.find_gacha_urls(game_data_dir)?; + find_gacha_url_and_validate_consistency(&ZenlessZoneZeroGacha, &facet, &uid, &gacha_urls) + .await? + } }; Ok(gacha_url.to_string()) @@ -102,6 +109,25 @@ async fn pull_all_gacha_records( ) .await? } + AccountFacet::ZenlessZoneZero => { + create_fetcher_channel( + ZenlessZoneZeroGacha, + reqwest, + ZenlessZoneZeroGacha, + gacha_url, + gacha_type_and_last_end_id_mappings, + |fragment| async { + window.emit(&event_channel, &fragment)?; + if save_to_storage { + if let GachaRecordFetcherChannelFragment::Data(data) = fragment { + storage.save_zzz_gacha_records(&data).await?; + } + } + Ok(()) + }, + ) + .await? + } } Ok(()) @@ -141,6 +167,10 @@ async fn import_gacha_records( let gacha_records = srgf::convert_srgf_to_offical(&mut srgf)?; storage.save_starrail_gacha_records(&gacha_records).await } + AccountFacet::ZenlessZoneZero => { + // TODO: Import ZZZ Gacha Records + todo!("Import ZZZ Gacha Records") + } } } @@ -167,6 +197,10 @@ async fn export_gacha_records( let (primary, format) = match facet { AccountFacet::Genshin => ("原神祈愿记录", "UIGF"), AccountFacet::StarRail => ("星穹铁道跃迁记录", "SRGF"), + AccountFacet::ZenlessZoneZero => { + // TODO: Export ZZZ Gacha Records + todo!("Export ZZZ Gacha Records") + } }; let filename = format!( "{}_{}_{}_{uid}_{time}.json", @@ -205,6 +239,10 @@ async fn export_gacha_records( let srgf = srgf::SRGF::new(uid, lang, time_zone, &now, srgf_list)?; srgf.to_writer(writer, false)?; } + AccountFacet::ZenlessZoneZero => { + // TODO: Export ZZZ Gacha Records + todo!("Export ZZZ Gacha Records") + } } Ok(filename) diff --git a/src-tauri/src/gacha/utilities.rs b/src-tauri/src/gacha/utilities.rs index 96325cee..92ce907c 100644 --- a/src-tauri/src/gacha/utilities.rs +++ b/src-tauri/src/gacha/utilities.rs @@ -253,6 +253,7 @@ pub(super) struct GachaResponse { pub(super) async fn fetch_gacha_records( reqwest: &Reqwest, + facet: &AccountFacet, endpoint: &str, gacha_url: &str, gacha_type: Option<&str>, @@ -266,14 +267,21 @@ pub(super) async fn fetch_gacha_records( .into_owned() .collect(); + let gacha_type_field: &'static str = if facet == &AccountFacet::ZenlessZoneZero { + "real_gacha_type" + } else { + "gacha_type" + }; + let origin_gacha_type = queries - .get("gacha_type") + .get(gacha_type_field) .cloned() .ok_or(Error::IllegalGachaUrl)?; + let origin_end_id = queries.get("end_id").cloned(); let gacha_type = gacha_type.unwrap_or(&origin_gacha_type); - queries.remove("gacha_type"); + queries.remove(gacha_type_field); queries.remove("page"); queries.remove("size"); queries.remove("begin_id"); @@ -285,7 +293,7 @@ pub(super) async fn fetch_gacha_records( .query_pairs_mut() .append_pair("page", "1") .append_pair("size", "20") - .append_pair("gacha_type", gacha_type); + .append_pair(gacha_type_field, gacha_type); if let Some(end_id) = end_id.or(origin_end_id.as_deref()) { url.query_pairs_mut().append_pair("end_id", end_id); diff --git a/src-tauri/src/storage/entity_account.rs b/src-tauri/src/storage/entity_account.rs index d2d033fe..bbc1725c 100644 --- a/src-tauri/src/storage/entity_account.rs +++ b/src-tauri/src/storage/entity_account.rs @@ -15,6 +15,9 @@ pub enum AccountFacet { #[sea_orm(string_value = "starrail")] #[serde(rename = "starrail")] StarRail, + #[sea_orm(string_value = "zzz")] + #[serde(rename = "zzz")] + ZenlessZoneZero, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] diff --git a/src-tauri/src/storage/entity_zzz_gacha_record.rs b/src-tauri/src/storage/entity_zzz_gacha_record.rs new file mode 100644 index 00000000..70c187da --- /dev/null +++ b/src-tauri/src/storage/entity_zzz_gacha_record.rs @@ -0,0 +1,67 @@ +use crate::gacha::ZenlessZoneZeroGachaRecord; +use sea_orm::entity::prelude::*; +use sea_orm::ActiveValue; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "zzz_gacha_records")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + #[sea_orm(indexed)] + pub uid: String, + #[sea_orm(indexed)] + pub gacha_id: String, + #[sea_orm(indexed)] + pub gacha_type: String, + #[sea_orm(indexed)] + pub item_id: String, + pub count: String, + pub time: String, + pub name: String, + pub lang: String, + pub item_type: String, + pub rank_type: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +/// Convert + +impl From for ActiveModel { + fn from(value: ZenlessZoneZeroGachaRecord) -> Self { + Self { + id: ActiveValue::set(value.id), + uid: ActiveValue::set(value.uid), + gacha_id: ActiveValue::set(value.gacha_id), + gacha_type: ActiveValue::set(value.gacha_type), + item_id: ActiveValue::set(value.item_id), + count: ActiveValue::set(value.count), + time: ActiveValue::set(value.time), + name: ActiveValue::set(value.name), + lang: ActiveValue::set(value.lang), + item_type: ActiveValue::set(value.item_type), + rank_type: ActiveValue::set(value.rank_type), + } + } +} + +impl From for ZenlessZoneZeroGachaRecord { + fn from(value: Model) -> Self { + Self { + id: value.id, + uid: value.uid, + gacha_id: value.gacha_id, + gacha_type: value.gacha_type, + item_id: value.item_id, + count: value.count, + time: value.time, + name: value.name, + lang: value.lang, + item_type: value.item_type, + rank_type: value.rank_type, + } + } +} diff --git a/src-tauri/src/storage/impl_storage.rs b/src-tauri/src/storage/impl_storage.rs index dd264670..ea84c2fe 100644 --- a/src-tauri/src/storage/impl_storage.rs +++ b/src-tauri/src/storage/impl_storage.rs @@ -10,12 +10,16 @@ use super::entity_starrail_gacha_record::{ ActiveModel as StarRailGachaRecordActiveModel, Column as StarRailGachaRecordColumn, Entity as StarRailGachaRecordEntity, }; +use super::entity_zzz_gacha_record::{ + ActiveModel as ZenlessZoneZeroGachaRecordActiveModel, Column as ZenlessZoneZeroGachaRecordColumn, + Entity as ZenlessZoneZeroGachaRecordEntity, +}; use super::utilities::{ create_index_statements, create_table_statement, execute_statements, is_constraint_unique_err, }; use crate::constants::DATABASE; use crate::error::{Error, Result}; -use crate::gacha::{GenshinGachaRecord, StarRailGachaRecord}; +use crate::gacha::{GenshinGachaRecord, StarRailGachaRecord, ZenlessZoneZeroGachaRecord}; use futures::TryStreamExt; use paste::paste; use sea_orm::sea_query::{Condition, Index, OnConflict}; @@ -77,18 +81,24 @@ impl Storage { debug!("Creating tables..."); let statement1 = create_table_statement(GenshinGachaRecordEntity); let statement2 = create_table_statement(StarRailGachaRecordEntity); - let statement3 = create_table_statement(AccountEntity); - execute_statements(&self.database, &[statement1, statement2, statement3]).await?; + let statement3 = create_table_statement(ZenlessZoneZeroGachaRecordEntity); + let statement4 = create_table_statement(AccountEntity); + execute_statements( + &self.database, + &[statement1, statement2, statement3, statement4], + ) + .await?; } { debug!("Creating indexes..."); let statement1 = create_index_statements(GenshinGachaRecordEntity); let statement2 = create_index_statements(StarRailGachaRecordEntity); - let statement3 = create_index_statements(AccountEntity); + let statement3 = create_index_statements(ZenlessZoneZeroGachaRecordEntity); + let statement4 = create_index_statements(AccountEntity); // Account: facet + uid constraint - let statement4 = Index::create() + let statement5 = Index::create() .name(&format!( "idx-{}-{}-{}", EntityName::table_name(&AccountEntity), @@ -105,7 +115,8 @@ impl Storage { let mut statements = statement1; statements.extend(statement2); statements.extend(statement3); - statements.push(statement4); + statements.extend(statement4); + statements.push(statement5); execute_statements(&self.database, &statements).await?; } @@ -371,6 +382,15 @@ impl_gacha_records_crud!( StarRailGachaRecordColumn ); +impl_gacha_records_crud!( + Storage, + zzz, + ZenlessZoneZeroGachaRecord, + ZenlessZoneZeroGachaRecordActiveModel, + ZenlessZoneZeroGachaRecordEntity, + ZenlessZoneZeroGachaRecordColumn +); + /// Tauri commands #[tauri::command] @@ -485,6 +505,7 @@ macro_rules! impl_gacha_records_tauri_command { impl_gacha_records_tauri_command!(genshin, GenshinGachaRecord); impl_gacha_records_tauri_command!(starrail, StarRailGachaRecord); +impl_gacha_records_tauri_command!(zzz, ZenlessZoneZeroGachaRecord); /// Tauri plugin @@ -536,7 +557,9 @@ impl StoragePluginBuilder { find_genshin_gacha_records, save_genshin_gacha_records, find_starrail_gacha_records, - save_starrail_gacha_records + save_starrail_gacha_records, + find_zzz_gacha_records, + save_zzz_gacha_records, ]) .build() } diff --git a/src-tauri/src/storage/mod.rs b/src-tauri/src/storage/mod.rs index 6b382552..c030c341 100644 --- a/src-tauri/src/storage/mod.rs +++ b/src-tauri/src/storage/mod.rs @@ -2,6 +2,7 @@ pub mod entity_account; pub mod entity_genshin_gacha_record; pub mod entity_genshin_gacha_record_legacy; pub mod entity_starrail_gacha_record; +pub mod entity_zzz_gacha_record; mod impl_storage; mod legacy_migration; diff --git a/src/assets/images/zzz/Bangboo.png b/src/assets/images/zzz/Bangboo.png new file mode 100644 index 00000000..f14fdbcf Binary files /dev/null and b/src/assets/images/zzz/Bangboo.png differ diff --git a/src/assets/images/zzz/Belle.png b/src/assets/images/zzz/Belle.png new file mode 100644 index 00000000..471f79a4 Binary files /dev/null and b/src/assets/images/zzz/Belle.png differ diff --git a/src/assets/images/zzz/Wise.png b/src/assets/images/zzz/Wise.png new file mode 100644 index 00000000..a0a34361 Binary files /dev/null and b/src/assets/images/zzz/Wise.png differ diff --git a/src/assets/images/zzz/bangboo/54001.png b/src/assets/images/zzz/bangboo/54001.png new file mode 100644 index 00000000..b5a04b52 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54001.png differ diff --git a/src/assets/images/zzz/bangboo/54002.png b/src/assets/images/zzz/bangboo/54002.png new file mode 100644 index 00000000..e25d4729 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54002.png differ diff --git a/src/assets/images/zzz/bangboo/54004.png b/src/assets/images/zzz/bangboo/54004.png new file mode 100644 index 00000000..5093fed8 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54004.png differ diff --git a/src/assets/images/zzz/bangboo/54005.png b/src/assets/images/zzz/bangboo/54005.png new file mode 100644 index 00000000..a944a27a Binary files /dev/null and b/src/assets/images/zzz/bangboo/54005.png differ diff --git a/src/assets/images/zzz/bangboo/54006.png b/src/assets/images/zzz/bangboo/54006.png new file mode 100644 index 00000000..699c08a2 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54006.png differ diff --git a/src/assets/images/zzz/bangboo/54008.png b/src/assets/images/zzz/bangboo/54008.png new file mode 100644 index 00000000..43b7eb54 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54008.png differ diff --git a/src/assets/images/zzz/bangboo/54009.png b/src/assets/images/zzz/bangboo/54009.png new file mode 100644 index 00000000..114d9528 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54009.png differ diff --git a/src/assets/images/zzz/bangboo/54013.png b/src/assets/images/zzz/bangboo/54013.png new file mode 100644 index 00000000..8ca58a67 Binary files /dev/null and b/src/assets/images/zzz/bangboo/54013.png differ diff --git a/src/assets/images/zzz/character/1021.png b/src/assets/images/zzz/character/1021.png new file mode 100644 index 00000000..d9ebbdba Binary files /dev/null and b/src/assets/images/zzz/character/1021.png differ diff --git a/src/assets/images/zzz/character/1041.png b/src/assets/images/zzz/character/1041.png new file mode 100644 index 00000000..3e6bd045 Binary files /dev/null and b/src/assets/images/zzz/character/1041.png differ diff --git a/src/assets/images/zzz/character/1091.png b/src/assets/images/zzz/character/1091.png new file mode 100644 index 00000000..9701a5f8 Binary files /dev/null and b/src/assets/images/zzz/character/1091.png differ diff --git a/src/assets/images/zzz/character/1101.png b/src/assets/images/zzz/character/1101.png new file mode 100644 index 00000000..cb6c51e3 Binary files /dev/null and b/src/assets/images/zzz/character/1101.png differ diff --git a/src/assets/images/zzz/character/1141.png b/src/assets/images/zzz/character/1141.png new file mode 100644 index 00000000..d021278c Binary files /dev/null and b/src/assets/images/zzz/character/1141.png differ diff --git a/src/assets/images/zzz/character/1181.png b/src/assets/images/zzz/character/1181.png new file mode 100644 index 00000000..cac41b7e Binary files /dev/null and b/src/assets/images/zzz/character/1181.png differ diff --git a/src/assets/images/zzz/character/1191.png b/src/assets/images/zzz/character/1191.png new file mode 100644 index 00000000..6c0c842d Binary files /dev/null and b/src/assets/images/zzz/character/1191.png differ diff --git a/src/assets/images/zzz/character/1211.png b/src/assets/images/zzz/character/1211.png new file mode 100644 index 00000000..b5b852be Binary files /dev/null and b/src/assets/images/zzz/character/1211.png differ diff --git a/src/assets/images/zzz/character/1241.png b/src/assets/images/zzz/character/1241.png new file mode 100644 index 00000000..997dcd4a Binary files /dev/null and b/src/assets/images/zzz/character/1241.png differ diff --git a/src/assets/images/zzz/weapon/14102.png b/src/assets/images/zzz/weapon/14102.png new file mode 100644 index 00000000..e8bcede1 Binary files /dev/null and b/src/assets/images/zzz/weapon/14102.png differ diff --git a/src/assets/images/zzz/weapon/14104.png b/src/assets/images/zzz/weapon/14104.png new file mode 100644 index 00000000..1b956aa8 Binary files /dev/null and b/src/assets/images/zzz/weapon/14104.png differ diff --git a/src/assets/images/zzz/weapon/14110.png b/src/assets/images/zzz/weapon/14110.png new file mode 100644 index 00000000..1dff3a03 Binary files /dev/null and b/src/assets/images/zzz/weapon/14110.png differ diff --git a/src/assets/images/zzz/weapon/14114.png b/src/assets/images/zzz/weapon/14114.png new file mode 100644 index 00000000..4487e139 Binary files /dev/null and b/src/assets/images/zzz/weapon/14114.png differ diff --git a/src/assets/images/zzz/weapon/14118.png b/src/assets/images/zzz/weapon/14118.png new file mode 100644 index 00000000..5673f1c6 Binary files /dev/null and b/src/assets/images/zzz/weapon/14118.png differ diff --git a/src/assets/images/zzz/weapon/14119.png b/src/assets/images/zzz/weapon/14119.png new file mode 100644 index 00000000..3602b17d Binary files /dev/null and b/src/assets/images/zzz/weapon/14119.png differ diff --git a/src/assets/images/zzz/weapon/14121.png b/src/assets/images/zzz/weapon/14121.png new file mode 100644 index 00000000..2f2a6d36 Binary files /dev/null and b/src/assets/images/zzz/weapon/14121.png differ diff --git a/src/assets/images/zzz/weapon/14124.png b/src/assets/images/zzz/weapon/14124.png new file mode 100644 index 00000000..015c6edb Binary files /dev/null and b/src/assets/images/zzz/weapon/14124.png differ diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 6495e812..76f9a14d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,6 +12,7 @@ import Typography from '@mui/material/Typography' import HomeIcon from '@mui/icons-material/Home' import StarIcon from '@mui/icons-material/Star' import DirectionsSubwayIcon from '@mui/icons-material/DirectionsSubway' +import LiveTvIcon from '@mui/icons-material/LiveTv' import SettingsIcon from '@mui/icons-material/Settings' import LogoSrc from '@/assets/images/Logo.png' @@ -42,7 +43,8 @@ type Nav = { title: string, href: string, icon?: React.ReactNode } const Navs: Nav[] = [ { title: '主页', href: '/', icon: }, { title: '祈愿', href: '/genshin', icon: }, - { title: '跃迁', href: '/starrail', icon: } + { title: '跃迁', href: '/starrail', icon: }, + { title: '调频', href: '/zzz', icon: } ] const NavSetting: Nav = @@ -84,7 +86,9 @@ const NavListItemButton = styled(Button, { paddingY: theme.spacing(0.5), display: 'inline-flex', flexDirection: 'column', - '& .MuiSvgIcon-root': { fontSize: '2rem' }, + '& .MuiSvgIcon-root': { + fontSize: '2rem' + }, ...(activated && { color: theme.palette.primary.main, backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity + 0.05), diff --git a/src/components/account/AccountAvatar.tsx b/src/components/account/AccountAvatar.tsx index 0ee04819..65a437ea 100644 --- a/src/components/account/AccountAvatar.tsx +++ b/src/components/account/AccountAvatar.tsx @@ -5,6 +5,7 @@ import AvatarGenshinLumine from '@/assets/images/genshin/UI_AvatarIcon_PlayerGir // eslint-disable-next-line @typescript-eslint/no-unused-vars import AvatarGenshinAether from '@/assets/images/genshin/UI_AvatarIcon_PlayerBoy.png' import AvatarStarRailTrailblazer from '@/assets/images/starrail/Trailblazer.png' +import AvatarZenlessBelle from '@/assets/images/zzz/Belle.png' export interface AccountAvatarProps extends Omit { facet: AccountFacet @@ -18,6 +19,8 @@ export default function AccountAvatar (props: AccountAvatarProps) { return AvatarGenshinLumine case AccountFacet.StarRail: return AvatarStarRailTrailblazer + case AccountFacet.ZenlessZoneZero: + return AvatarZenlessBelle default: return undefined } diff --git a/src/components/account/AccountMenuDialog.tsx b/src/components/account/AccountMenuDialog.tsx index aa990dc0..08f80434 100644 --- a/src/components/account/AccountMenuDialog.tsx +++ b/src/components/account/AccountMenuDialog.tsx @@ -207,7 +207,9 @@ function AccountMenuDialogForm (props: AccountMenuDialogFormProps) { const onSubmit = React.useCallback>(async (data) => { const uid = Number(data.uid) - if (uid < 1_0000_0000) { + + const isZZZ = facet === AccountFacet.ZenlessZoneZero + if ((isZZZ && uid < 10_000_000) || (!isZZZ && uid < 100_000_000)) { setError('uid', { message: '请输入正确的 UID 值!' }) return } @@ -227,7 +229,7 @@ function AccountMenuDialogForm (props: AccountMenuDialogFormProps) { } finally { setBusy(false) } - }, [setBusy, onSuccess, setError, isEdit, handleCreateAccount, handleUpdateAccount]) + }, [facet, setBusy, onSuccess, setError, isEdit, handleCreateAccount, handleUpdateAccount]) return (
@@ -241,7 +243,13 @@ function AccountMenuDialogForm (props: AccountMenuDialogFormProps) { InputProps={{ ...register('uid', { required: '请填写账号 UID 字段!', - validate: value => /^[1-9][0-9]{8}$/.test(value) || '请输入正确的 UID 值!' + validate: value => { + const isZZZ = facet === AccountFacet.ZenlessZoneZero + return (isZZZ + ? +value >= 10_000_000 + : /^[1-9][0-9]{8}$/.test(value) + ) || '请输入正确的 UID 值!' + } }), onKeyPress: numericOnly, startAdornment: ( @@ -311,7 +319,8 @@ function AccountMenuDialogForm (props: AccountMenuDialogFormProps) { const FacetGameDataDirExamples: Record = { [AccountFacet.Genshin]: 'D:/Genshin Impact/Genshin Impact Game/YuanShen_Data', - [AccountFacet.StarRail]: 'D:/StarRail/Game/StarRail_Data' + [AccountFacet.StarRail]: 'D:/StarRail/Game/StarRail_Data', + [AccountFacet.ZenlessZoneZero]: 'D:/ZenlessZoneZero Game/ZenlessZoneZero_Data' } function numericOnly (evt: React.KeyboardEvent) { diff --git a/src/components/gacha/GachaItemView.tsx b/src/components/gacha/GachaItemView.tsx index 23e88bba..02cf69bf 100644 --- a/src/components/gacha/GachaItemView.tsx +++ b/src/components/gacha/GachaItemView.tsx @@ -13,7 +13,7 @@ export interface GachaItemViewProps { facet: AccountFacet name: string id: string - isWeapon: boolean + itemType: string rank: 3 | 4 | 5 | '3' | '4' | '5' size: number usedPity?: number @@ -22,9 +22,9 @@ export interface GachaItemViewProps { } export default function GachaItemView (props: GachaItemViewProps) { - const { facet, name, id, isWeapon, rank, size, usedPity, restricted, time } = props + const { facet, name, id, itemType, rank, size, usedPity, restricted, time } = props - const category = isWeapon ? 'weapon' : 'character' + const category = ItemTypeCategoryMappings[itemType] const icon = lookupAssetIcon(facet, category, id) let src = icon?.[1] @@ -42,6 +42,7 @@ export default function GachaItemView (props: GachaItemViewProps) { data-facet={facet} data-rank={rank} data-restricted={restricted} + data-category={category} title={title} > {name} @@ -51,6 +52,15 @@ export default function GachaItemView (props: GachaItemViewProps) { ) } +const ItemTypeCategoryMappings: Record = { + 角色: 'character', + 武器: 'weapon', + 光锥: 'weapon', + 代理人: 'character', + 音擎: 'weapon', + 邦布: 'bangboo' +} + function getRemoteResourceSrc (facet: AccountFacet, category: string, itemIdOrName: string) { return `https://hoyo-gacha.lgou2w.com/static/${facet}/${category}/cutted/${itemIdOrName}.png` } @@ -89,7 +99,9 @@ const GachaItemViewSx: SxProps = { bottom: 0, border: 1, borderColor: 'rgba(255, 255, 255, 0.25)' - }, + } + }, + '&[data-facet="starrail"], &[data-facet="zzz"]': { '&[data-rank="3"] > img': { backgroundImage: 'linear-gradient(#434e7e, #4d80c8)' }, '&[data-rank="4"] > img': { backgroundImage: 'linear-gradient(#4e4976, #9061d2)' }, '&[data-rank="5"] > img': { backgroundImage: 'linear-gradient(#986359, #d2ad70)' } diff --git a/src/components/gacha/analysis/GachaAnalysisHistory.tsx b/src/components/gacha/analysis/GachaAnalysisHistory.tsx index 6f1e3c17..0e81425b 100644 --- a/src/components/gacha/analysis/GachaAnalysisHistory.tsx +++ b/src/components/gacha/analysis/GachaAnalysisHistory.tsx @@ -11,7 +11,7 @@ import Divider from '@mui/material/Divider' export default function GachaAnalysisHistory () { const { facet, gachaRecords } = useGachaLayoutContext() - const { namedValues: { character, weapon, permanent, newbie, anthology } } = gachaRecords + const { namedValues: { character, weapon, permanent, newbie, anthology, bangboo } } = gachaRecords return ( @@ -30,6 +30,9 @@ export default function GachaAnalysisHistory () { )} + {bangboo && bangboo.metadata.golden.sum > 0 && ( + + )} {newbie.metadata.golden.sum > 0 && ( )} @@ -49,7 +52,7 @@ function GachaAnalysisHistoryList ({ facet, value }: { {categoryTitle} - {category !== 'permanent' && category !== 'newbie' + {category !== 'permanent' && category !== 'newbie' && category !== 'bangboo' ? `${golden.sumRestricted} + ${golden.sum - golden.sumRestricted}` : golden.sum } @@ -63,7 +66,7 @@ function GachaAnalysisHistoryList ({ facet, value }: { key={item.id} name={item.name} id={item.item_id || item.name} - isWeapon={item.item_type === '武器' || item.item_type === '光锥'} + itemType={item.item_type} rank={5} size={GachaAnalysisHistoryItemViewSize} usedPity={item.usedPity} diff --git a/src/components/gacha/analysis/GachaAnalysisSum.tsx b/src/components/gacha/analysis/GachaAnalysisSum.tsx index 3bcc22fa..47500620 100644 --- a/src/components/gacha/analysis/GachaAnalysisSum.tsx +++ b/src/components/gacha/analysis/GachaAnalysisSum.tsx @@ -5,10 +5,12 @@ import { SxProps, Theme } from '@mui/material/styles' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' +import { AccountFacet } from '@/interfaces/account' export default function GachaAnalysisSum () { - const { gachaRecords } = useGachaLayoutContext() - const { namedValues: { character, weapon, permanent, newbie, anthology }, aggregatedValues } = gachaRecords + const { facet, gachaRecords } = useGachaLayoutContext() + const { namedValues: { character, weapon, permanent, newbie, anthology, bangboo }, aggregatedValues } = gachaRecords + const isZZZ = facet === AccountFacet.ZenlessZoneZero return ( @@ -27,8 +29,12 @@ export default function GachaAnalysisSum () { {anthology && anthology.total > 0 && } + {bangboo && bangboo.total > 0 && } {newbie.total > 0 && } - + ) diff --git a/src/components/gacha/chart/GachaChartCalendar.tsx b/src/components/gacha/chart/GachaChartCalendar.tsx index 79114aca..e3687de1 100644 --- a/src/components/gacha/chart/GachaChartCalendar.tsx +++ b/src/components/gacha/chart/GachaChartCalendar.tsx @@ -1,6 +1,7 @@ import React from 'react' import { useTheme } from '@mui/material/styles' import { resolveCurrency } from '@/interfaces/account' +import { isRankTypeOfBlue, isRankTypeOfPurple, isRankTypeOfGolden } from '@/hooks/useGachaRecordsQuery' import { useGachaLayoutContext } from '@/components/gacha/GachaLayoutContext' import { CalendarDatum, ResponsiveTimeRange } from '@nivo/calendar' import Stack from '@mui/material/Stack' @@ -10,19 +11,23 @@ import Typography from '@mui/material/Typography' import dayjs from '@/utilities/dayjs' export default function GachaChartCalendar () { - const { facet, gachaRecords: { aggregatedValues } } = useGachaLayoutContext() + const { facet, gachaRecords: { aggregatedValues, namedValues: { bangboo } } } = useGachaLayoutContext() const { action: currencyAction } = resolveCurrency(facet) const calendars = Object - .entries(aggregatedValues.values.reduce((acc, cur) => { - const key = dayjs(cur.time).format('YYYY-MM-DD') - if (!acc[key]) { - acc[key] = 1 - } else { - acc[key] += 1 - } - return acc - }, {} as Record)) + .entries(Array + .from(aggregatedValues.values) + .concat(bangboo?.values || []) + .reduce((acc, cur) => { + const key = dayjs(cur.time).format('YYYY-MM-DD') + if (!acc[key]) { + acc[key] = 1 + } else { + acc[key] += 1 + } + return acc + }, {} as Record) + ) .reduce((acc, [key, value]) => { acc.push({ day: key, value }) return acc @@ -35,11 +40,11 @@ export default function GachaChartCalendar () { if (!metadataByDay[day]) { metadataByDay[day] = { golden: 0, purple: 0, blue: 0 } } - if (record.rank_type === '5') { + if (isRankTypeOfGolden(facet, record)) { metadataByDay[day].golden += 1 - } else if (record.rank_type === '4') { + } else if (isRankTypeOfPurple(facet, record)) { metadataByDay[day].purple += 1 - } else if (record.rank_type === '3') { + } else if (isRankTypeOfBlue(facet, record)) { metadataByDay[day].blue += 1 } } diff --git a/src/components/gacha/chart/GachaChartPie.tsx b/src/components/gacha/chart/GachaChartPie.tsx index 8070c6b4..650530e1 100644 --- a/src/components/gacha/chart/GachaChartPie.tsx +++ b/src/components/gacha/chart/GachaChartPie.tsx @@ -8,17 +8,26 @@ import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' export default function GachaChartCalendar () { - const { facet, gachaRecords: { aggregatedValues, namedValues: { character, weapon, permanent, newbie, anthology } } } = useGachaLayoutContext() - - const itemTypesData = aggregatedValues.values.reduce((acc, cur) => { - const key = cur.item_type - if (!acc[key]) { - acc[key] = 1 - } else { - acc[key] += 1 + const { + facet, + gachaRecords: { + aggregatedValues, + namedValues: { character, weapon, permanent, newbie, anthology, bangboo } } - return acc - }, {} as Record) + } = useGachaLayoutContext() + + const itemTypesData = Array + .from(aggregatedValues.values) + .concat(bangboo?.values || []) + .reduce((acc, cur) => { + const key = cur.item_type + if (!acc[key]) { + acc[key] = 1 + } else { + acc[key] += 1 + } + return acc + }, {} as Record) return ( @@ -30,9 +39,18 @@ export default function GachaChartCalendar () { @@ -42,13 +60,25 @@ export default function GachaChartCalendar () { @@ -59,16 +89,31 @@ export default function GachaChartCalendar () { {...PieProps} arcLabelsSkipAngle={10} arcLinkLabelsSkipAngle={10} - data={[ - { id: '角色', value: character.total }, - { - id: facet === AccountFacet.Genshin ? '武器' : '光锥', - value: weapon.total - }, - { id: '常驻', value: permanent.total }, - { id: '新手', value: newbie.total }, - ...(anthology ? [{ id: '集录', value: anthology.total }] : []) - ]} + data={ + facet === AccountFacet.Genshin + ? [ + { id: '角色', value: character.total }, + { id: '武器', value: weapon.total }, + { id: '常驻', value: permanent.total }, + { id: '新手', value: newbie.total }, + { id: '集录', value: anthology?.total || 0 } + ] + : facet === AccountFacet.StarRail + ? [ + { id: '角色', value: character.total }, + { id: '光锥', value: weapon.total }, + { id: '常驻', value: permanent.total }, + { id: '新手', value: newbie.total } + ] + : facet === AccountFacet.ZenlessZoneZero + ? [ + { id: '独家', value: character.total }, + { id: '音擎', value: weapon.total }, + { id: '常驻', value: permanent.total }, + { id: '邦布', value: bangboo?.total || 0 } + ] + : [] + } /> diff --git a/src/components/gacha/icons.tsx b/src/components/gacha/icons.tsx index 3236fbc0..13516d7b 100644 --- a/src/components/gacha/icons.tsx +++ b/src/components/gacha/icons.tsx @@ -145,6 +145,31 @@ import StarRailWeapon23025 from '@/assets/images/starrail/weapon/23025.png' import StarRailWeapon23026 from '@/assets/images/starrail/weapon/23026.png' import StarRailWeapon23027 from '@/assets/images/starrail/weapon/23027.png' import StarRailWeapon23028 from '@/assets/images/starrail/weapon/23028.png' +import ZenlessCharacter1021 from '@/assets/images/zzz/character/1021.png' +import ZenlessCharacter1041 from '@/assets/images/zzz/character/1041.png' +import ZenlessCharacter1091 from '@/assets/images/zzz/character/1091.png' +import ZenlessCharacter1101 from '@/assets/images/zzz/character/1101.png' +import ZenlessCharacter1141 from '@/assets/images/zzz/character/1141.png' +import ZenlessCharacter1181 from '@/assets/images/zzz/character/1181.png' +import ZenlessCharacter1191 from '@/assets/images/zzz/character/1191.png' +import ZenlessCharacter1211 from '@/assets/images/zzz/character/1211.png' +import ZenlessCharacter1241 from '@/assets/images/zzz/character/1241.png' +import ZenlessWeapon14102 from '@/assets/images/zzz/weapon/14102.png' +import ZenlessWeapon14104 from '@/assets/images/zzz/weapon/14104.png' +import ZenlessWeapon14110 from '@/assets/images/zzz/weapon/14110.png' +import ZenlessWeapon14114 from '@/assets/images/zzz/weapon/14114.png' +import ZenlessWeapon14118 from '@/assets/images/zzz/weapon/14118.png' +import ZenlessWeapon14119 from '@/assets/images/zzz/weapon/14119.png' +import ZenlessWeapon14121 from '@/assets/images/zzz/weapon/14121.png' +import ZenlessWeapon14124 from '@/assets/images/zzz/weapon/14124.png' +import ZenlessBangboo54001 from '@/assets/images/zzz/bangboo/54001.png' +import ZenlessBangboo54002 from '@/assets/images/zzz/bangboo/54002.png' +import ZenlessBangboo54004 from '@/assets/images/zzz/bangboo/54004.png' +import ZenlessBangboo54005 from '@/assets/images/zzz/bangboo/54005.png' +import ZenlessBangboo54006 from '@/assets/images/zzz/bangboo/54006.png' +import ZenlessBangboo54008 from '@/assets/images/zzz/bangboo/54008.png' +import ZenlessBangboo54009 from '@/assets/images/zzz/bangboo/54009.png' +import ZenlessBangboo54013 from '@/assets/images/zzz/bangboo/54013.png' // HACK: These static resources only contain five-star! @@ -309,14 +334,49 @@ const StarRail = { } } as const +const Zenless = { + character: { + 1021: [1021, ZenlessCharacter1021], + 1041: [1041, ZenlessCharacter1041], + 1091: [1091, ZenlessCharacter1091], + 1101: [1101, ZenlessCharacter1101], + 1141: [1141, ZenlessCharacter1141], + 1181: [1181, ZenlessCharacter1181], + 1191: [1191, ZenlessCharacter1191], + 1211: [1211, ZenlessCharacter1211], + 1241: [1241, ZenlessCharacter1241] + }, + weapon: { + 14102: [14102, ZenlessWeapon14102], + 14104: [14104, ZenlessWeapon14104], + 14110: [14110, ZenlessWeapon14110], + 14114: [14114, ZenlessWeapon14114], + 14118: [14118, ZenlessWeapon14118], + 14119: [14119, ZenlessWeapon14119], + 14121: [14121, ZenlessWeapon14121], + 14124: [14124, ZenlessWeapon14124] + }, + bangboo: { + 54001: [54001, ZenlessBangboo54001], + 54002: [54002, ZenlessBangboo54002], + 54004: [54004, ZenlessBangboo54004], + 54005: [54005, ZenlessBangboo54005], + 54006: [54006, ZenlessBangboo54006], + 54008: [54008, ZenlessBangboo54008], + 54009: [54009, ZenlessBangboo54009], + 54013: [54013, ZenlessBangboo54013] + } +} as const + const Assets = { genshin: Genshin, - starrail: StarRail + starrail: StarRail, + zzz: Zenless } export function lookupAssetIcon ( facet: keyof typeof Assets, - category: 'character' | 'weapon', + category: 'character' | 'weapon' | 'bangboo', nameOrId: string ): [number, string] | undefined { // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/src/components/gacha/overview/GachaOverviewGrid.tsx b/src/components/gacha/overview/GachaOverviewGrid.tsx index 8867edd7..4af7e0af 100644 --- a/src/components/gacha/overview/GachaOverviewGrid.tsx +++ b/src/components/gacha/overview/GachaOverviewGrid.tsx @@ -13,8 +13,9 @@ import dayjs from '@/utilities/dayjs' export default function GachaOverviewGrid () { const { facet, gachaRecords } = useGachaLayoutContext() - const { namedValues: { character, weapon, permanent, newbie, anthology }, aggregatedValues } = gachaRecords + const { namedValues: { character, weapon, permanent, newbie, anthology, bangboo }, aggregatedValues } = gachaRecords const hasAnthology = !!anthology && anthology.total > 0 + const hasBangboo = facet === AccountFacet.ZenlessZoneZero && !!bangboo && bangboo.total > 0 return ( @@ -33,7 +34,12 @@ export default function GachaOverviewGrid () { )} - + {hasBangboo && ( + + + + )} + @@ -58,6 +64,8 @@ function GachaOverviewGridCard ({ facet, value, newbie }: { const newbieGoldenName = newbieGolden && `${newbieGolden.name}` const aggregated = category === 'aggregated' + const isZZZ = facet === AccountFacet.ZenlessZoneZero + const isBangboo = isZZZ && category === 'bangboo' return ( @@ -67,12 +75,17 @@ function GachaOverviewGridCard ({ facet, value, newbie }: { {categoryTitle} - {aggregated && (包含新手)} + {aggregated && ( + + {isZZZ ? '(不含邦布)' : '(包含新手)'} + + )} - {dayjs(firstTime).format('YYYY.MM.DD')} - {' - '} - {dayjs(lastTime).format('YYYY.MM.DD')} + {firstTime && lastTime + ? dayjs(firstTime).format('YYYY.MM.DD') + ' - ' + dayjs(lastTime).format('YYYY.MM.DD') + :   + } @@ -90,7 +103,9 @@ function GachaOverviewGridCard ({ facet, value, newbie }: { - + {!isBangboo && ( + + )} {lastGolden && !aggregated && ( @@ -100,7 +115,7 @@ function GachaOverviewGridCard ({ facet, value, newbie }: { key={lastGolden.id} name={lastGolden.name} id={lastGolden.item_id || lastGolden.name} - isWeapon={lastGolden.item_type === '武器' || lastGolden.item_type === '光锥'} + itemType={lastGolden.item_type} rank={5} size={72} usedPity={lastGolden.usedPity} diff --git a/src/components/gacha/overview/GachaOverviewTags.tsx b/src/components/gacha/overview/GachaOverviewTags.tsx index 32fe7b98..6b601a76 100644 --- a/src/components/gacha/overview/GachaOverviewTags.tsx +++ b/src/components/gacha/overview/GachaOverviewTags.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' import { useGachaLayoutContext } from '@/components/gacha/GachaLayoutContext' -import { resolveCurrency } from '@/interfaces/account' +import { AccountFacet, resolveCurrency } from '@/interfaces/account' import { SxProps, Theme } from '@mui/material' import Stack from '@mui/material/Stack' import Box from '@mui/material/Box' @@ -15,7 +15,7 @@ export default function GachaOverviewTags () { const sortByUsedPity = Array.from(aggregatedValues.metadata.golden.values).sort((a, b) => a.usedPity - b.usedPity) const luck = sortByUsedPity[0] - const unluck = sortByUsedPity[sortByUsedPity.length - 1] + const unluck = sortByUsedPity.length > 1 && sortByUsedPity[sortByUsedPity.length - 1] const countGroups = aggregatedValues.metadata.golden.values.reduce((acc, value) => { (acc[value.name] || (acc[value.name] = [])).push(value) @@ -58,6 +58,9 @@ export default function GachaOverviewTags () { {`❖ ${action}标签`} + {facet === AccountFacet.ZenlessZoneZero && ( + (不含邦布) + )} {luck && } diff --git a/src/components/gacha/overview/GachaOverviewTooltips.tsx b/src/components/gacha/overview/GachaOverviewTooltips.tsx index d11a8367..d0ac6f69 100644 --- a/src/components/gacha/overview/GachaOverviewTooltips.tsx +++ b/src/components/gacha/overview/GachaOverviewTooltips.tsx @@ -6,9 +6,9 @@ import Typography from '@mui/material/Typography' import dayjs from '@/utilities/dayjs' export default function GachaOverviewTooltips () { - const { facet, gachaRecords } = useGachaLayoutContext() - const { total, firstTime, lastTime } = gachaRecords + const { facet, gachaRecords: { aggregatedValues } } = useGachaLayoutContext() const { currency, action } = resolveCurrency(facet) + const { total, firstTime, lastTime } = aggregatedValues return ( diff --git a/src/components/gacha/toolbar/GachaActionExport.tsx b/src/components/gacha/toolbar/GachaActionExport.tsx index 5f8192d2..56d71ef2 100644 --- a/src/components/gacha/toolbar/GachaActionExport.tsx +++ b/src/components/gacha/toolbar/GachaActionExport.tsx @@ -78,7 +78,8 @@ export default function GachaActionExport () { {{ [AccountFacet.Genshin]: UIGF 统一可交换祈愿记录标准 v2.2, - [AccountFacet.StarRail]: SRGF 星穹铁道抽卡记录标准 v1.0 + [AccountFacet.StarRail]: SRGF 星穹铁道抽卡记录标准 v1.0, + [AccountFacet.ZenlessZoneZero]: ? }[facet]} {/* diff --git a/src/components/gacha/toolbar/GachaActionFetch.tsx b/src/components/gacha/toolbar/GachaActionFetch.tsx index 96cb7ffd..8c56a4c3 100644 --- a/src/components/gacha/toolbar/GachaActionFetch.tsx +++ b/src/components/gacha/toolbar/GachaActionFetch.tsx @@ -35,7 +35,7 @@ export default function GachaActionFetch () { const { facet, uid, gachaUrl } = selectedAccount try { - const { namedValues: { character, weapon, permanent, newbie, anthology } } = gachaRecords + const { namedValues: { character, weapon, permanent, newbie, anthology, bangboo } } = gachaRecords const pullNewbie = shouldPullNewbie(facet, newbie) const fragments = await pull(facet, uid, { gachaUrl, @@ -44,6 +44,7 @@ export default function GachaActionFetch () { [weapon.gachaType]: weapon.lastEndId ?? null, [permanent.gachaType]: permanent.lastEndId ?? null, ...(anthology ? { [anthology.gachaType]: anthology.lastEndId ?? null } : {}), + ...(bangboo ? { [bangboo.gachaType]: bangboo.lastEndId ?? null } : {}), ...(pullNewbie || {}) }, eventChannel: 'gachaRecords-fetcher-event-channel', @@ -150,10 +151,13 @@ function shouldPullNewbie ( // HACK: // Genshin Impact : Newbie Gacha Pool = 20 times // Honkai: Star Rail : = 50 times + // Zenless Zone Zero : Useless if (facet === AccountFacet.Genshin && newbie.total >= 20) { return null } else if (facet === AccountFacet.StarRail && newbie.total >= 50) { return null + } else if (facet === AccountFacet.ZenlessZoneZero) { + return null } else { return { [newbie.gachaType]: newbie.lastEndId ?? null diff --git a/src/components/gacha/toolbar/GachaActionImport.tsx b/src/components/gacha/toolbar/GachaActionImport.tsx index 0c6ab485..8f772864 100644 --- a/src/components/gacha/toolbar/GachaActionImport.tsx +++ b/src/components/gacha/toolbar/GachaActionImport.tsx @@ -29,7 +29,8 @@ export default function GachaActionImport () { extensions: ['json'], name: { [AccountFacet.Genshin]: 'UIGF 统一可交换祈愿记录标准 v2.2', - [AccountFacet.StarRail]: 'SRGF 星穹铁道抽卡记录标准 v1.0' + [AccountFacet.StarRail]: 'SRGF 星穹铁道抽卡记录标准 v1.0', + [AccountFacet.ZenlessZoneZero]: '?' }[selectedAccount.facet] }] }) diff --git a/src/components/gacha/toolbar/index.tsx b/src/components/gacha/toolbar/index.tsx index 4e42c8e1..b31f6652 100644 --- a/src/components/gacha/toolbar/index.tsx +++ b/src/components/gacha/toolbar/index.tsx @@ -13,17 +13,19 @@ export interface GachaToolbarProps { } export default function GachaToolbar (props: GachaToolbarProps) { - const { ActionTabsProps } = props + const { facet, ActionTabsProps } = props return ( - - - - + {facet !== AccountFacet.ZenlessZoneZero && ( + + + + + )} ) diff --git a/src/components/setting/SettingAbout.tsx b/src/components/setting/SettingAbout.tsx index 01e453e3..c49d5d9c 100644 --- a/src/components/setting/SettingAbout.tsx +++ b/src/components/setting/SettingAbout.tsx @@ -37,14 +37,13 @@ export default function SettingAbout () { MUI {' 框架开发。'}
- {'本软件不会收集任何用户数据。所产生的数据(包括但不限于使用数据、抽卡数据、账号信息等)均保存在用户本地。'} -
- {'软件的部分图片资源来源于「原神」、「崩坏:星穹铁道」©miHoYo 上海米哈游影铁科技有限公司 版权所有。'} -
- {'软件使用的字体资源「汉仪文黑-85W」©北京汉仪创新科技股份有限公司 版权所有。'} -
- {'代码完全开源。仅供个人学习交流使用。请勿用于任何商业或违法违规用途。'}
+ + {'本软件不会向您索要任何关于 ©miHoYo 账户的账号密码信息,也不会收集任何用户数据。所产生的数据(包括但不限于使用数据、抽卡数据、UID 信息等)均保存在用户本地。'} + {'软件的部分图片资源来源于「原神」、「崩坏:星穹铁道」、「绝区零」©miHoYo 上海米哈游影铁科技有限公司 版权所有。'} + {'软件使用的字体资源「汉仪文黑-85W」©北京汉仪创新科技股份有限公司 版权所有。'} + {'代码完全开源。仅供个人学习交流使用。请勿用于任何商业或违法违规用途。'} +
diff --git a/src/hooks/useGachaRecordsFetcher.ts b/src/hooks/useGachaRecordsFetcher.ts index a7ce5e1e..e0c436dc 100644 --- a/src/hooks/useGachaRecordsFetcher.ts +++ b/src/hooks/useGachaRecordsFetcher.ts @@ -1,14 +1,14 @@ import React from 'react' import { event } from '@tauri-apps/api' import { useImmer } from 'use-immer' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' +import { GenshinGachaRecord, StarRailGachaRecord, ZenlessZoneZeroGachaRecord } from '@/interfaces/gacha' import PluginGacha from '@/utilities/plugin-gacha' type Fragment = 'sleeping' | { ready: string } | { pagination: number } | - { data: Array } | + { data: Array } | 'finished' export default function useGachaRecordsFetcher () { diff --git a/src/hooks/useGachaRecordsQuery.ts b/src/hooks/useGachaRecordsQuery.ts index 52cb7c07..d4d9d6d7 100644 --- a/src/hooks/useGachaRecordsQuery.ts +++ b/src/hooks/useGachaRecordsQuery.ts @@ -3,10 +3,10 @@ import React from 'react' import { QueryKey, FetchQueryOptions, useQuery, useQueryClient } from '@tanstack/react-query' import { AccountFacet, Account, resolveCurrency } from '@/interfaces/account' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' +import { GenshinGachaRecord, StarRailGachaRecord, ZenlessZoneZeroGachaRecord } from '@/interfaces/gacha' import PluginStorage from '@/utilities/plugin-storage' -type GachaRecord = GenshinGachaRecord | StarRailGachaRecord +type GachaRecord = GenshinGachaRecord | StarRailGachaRecord | ZenlessZoneZeroGachaRecord // Computed Gacha Records // See below @@ -15,8 +15,9 @@ export interface GachaRecords { readonly uid: Account['uid'] readonly gachaTypeToCategoryMappings: Record readonly values: Partial> - readonly namedValues: Omit, 'anthology'> + readonly namedValues: Omit, 'anthology' | 'bangboo'> & { 'anthology'?: NamedGachaRecords } // Genshin Impact only + & { 'bangboo'?: NamedGachaRecords } // Zenless Zone Zero only readonly aggregatedValues: Omit readonly total: number readonly firstTime?: GachaRecord['time'] @@ -24,7 +25,7 @@ export interface GachaRecords { } export interface NamedGachaRecords { - category: 'newbie' | 'permanent' | 'character' | 'weapon' | 'anthology' + category: 'newbie' | 'permanent' | 'character' | 'weapon' | 'anthology' | 'bangboo' categoryTitle: string gachaType: GachaRecord['gacha_type'] lastEndId?: GachaRecord['id'] @@ -156,8 +157,17 @@ const KnownStarRailGachaTypes: Record = { + 0: 'newbie', // Avoid undefined metadata + 1: 'permanent', + 2: 'character', + 3: 'weapon', + 5: 'bangboo' +} + const KnownCategoryTitles: Record> = { [AccountFacet.Genshin]: { + bangboo: '', // Useless anthology: '集录', character: '角色活动', weapon: '武器活动', @@ -165,17 +175,30 @@ const KnownCategoryTitles: Record record.rank_type === '3' -const isRankTypeOfPurple = (record: GachaRecord) => record.rank_type === '4' -const isRankTypeOfGolden = (record: GachaRecord) => record.rank_type === '5' +export const isRankTypeOfBlue = (facet: AccountFacet, record: GachaRecord) => + record.rank_type === (facet === AccountFacet.ZenlessZoneZero ? '2' : '3') +export const isRankTypeOfPurple = (facet: AccountFacet, record: GachaRecord) => + record.rank_type === (facet === AccountFacet.ZenlessZoneZero ? '3' : '4') +export const isRankTypeOfGolden = (facet: AccountFacet, record: GachaRecord) => + record.rank_type === (facet === AccountFacet.ZenlessZoneZero ? '4' : '5') + const sortGachaRecordById = (a: GachaRecord, b: GachaRecord) => a.id.localeCompare(b.id) function concatNamedGachaRecordsValues ( @@ -199,9 +222,23 @@ function computeNamedGachaRecords ( facet: AccountFacet, values: GachaRecords['values'] ): GachaRecords['namedValues'] { - const categories = facet === AccountFacet.Genshin ? KnownGenshinGachaTypes : KnownStarRailGachaTypes const { action: currencyAction } = resolveCurrency(facet) + let categories: Record + switch (facet) { + case AccountFacet.Genshin: + categories = KnownGenshinGachaTypes + break + case AccountFacet.StarRail: + categories = KnownStarRailGachaTypes + break + case AccountFacet.ZenlessZoneZero: + categories = KnownZenlessZoneZeroGachaTypes + break + default: + throw new Error(`Unsupported account facet: ${facet}`) + } + return Object .entries(categories) .reduce((acc, [gachaType, category]) => { @@ -212,8 +249,8 @@ function computeNamedGachaRecords ( const firstTime = data[0]?.time const lastTime = data[total - 1]?.time const metadata: NamedGachaRecords['metadata'] = { - blue: computeGachaRecordsMetadata(total, data.filter(isRankTypeOfBlue)), - purple: computeGachaRecordsMetadata(total, data.filter(isRankTypeOfPurple)), + blue: computeGachaRecordsMetadata(total, data.filter((v) => isRankTypeOfBlue(facet, v))), + purple: computeGachaRecordsMetadata(total, data.filter((v) => isRankTypeOfPurple(facet, v))), golden: computeGoldenGachaRecordsMetadata(facet, data) } @@ -237,6 +274,11 @@ function computeAggregatedGachaRecords ( data: GachaRecord[], namedValues: GachaRecords['namedValues'] ): GachaRecords['aggregatedValues'] { + // HACK: Bangboo is a completely separate gacha pool and doesn't count towards the aggregated. + if (facet === AccountFacet.ZenlessZoneZero) { + data = data.filter((v) => v.gacha_type !== '5') + } + const total = data.length const firstTime = data[0]?.time const lastTime = data[total - 1]?.time @@ -250,7 +292,7 @@ function computeAggregatedGachaRecords ( (anthology ? anthology.metadata.blue.sum : 0) const blueSumPercentage = blueSum > 0 ? Math.round(blueSum / total * 10000) / 100 : 0 - const blueValues = data.filter(isRankTypeOfBlue) + const blueValues = data.filter((v) => isRankTypeOfBlue(facet, v)) const purpleSum = newbie.metadata.purple.sum + @@ -260,7 +302,7 @@ function computeAggregatedGachaRecords ( (anthology ? anthology.metadata.purple.sum : 0) const purpleSumPercentage = purpleSum > 0 ? Math.round(purpleSum / total * 10000) / 100 : 0 - const purpleValues = data.filter(isRankTypeOfPurple) + const purpleValues = data.filter((v) => isRankTypeOfPurple(facet, v)) const goldenSum = newbie.metadata.golden.sum + @@ -343,7 +385,7 @@ function computeGoldenGachaRecordsMetadata ( let sumRestricted = 0 for (const record of values) { - const isGolden = isRankTypeOfGolden(record) + const isGolden = isRankTypeOfGolden(facet, record) pity += 1 if (isGolden) { @@ -383,6 +425,13 @@ function isRestrictedGolden ( return !KnownGenshinPermanentGoldenNames.includes(record.name) case AccountFacet.StarRail: return !KnownStarRailPermanentGoldenItemIds.includes(record.item_id) + case AccountFacet.ZenlessZoneZero: + // HACK: Bangboo no need! + if (record.gacha_type === '5') { + return false + } else { + return !KnownZenlessZoneZeroPermanentGoldenItemIds.includes(record.item_id) + } default: throw new Error(`Unknown facet: ${facet}`) } @@ -401,3 +450,8 @@ const KnownStarRailPermanentGoldenItemIds: string[] = [ '1003', '1004', '1101', '1104', '1107', '1209', '1211', '23000', '23002', '23003', '23004', '23005', '23012', '23013' ] + +const KnownZenlessZoneZeroPermanentGoldenItemIds: string[] = [ + '1021', '1041', '1101', '1141', '1181', '1211', + '14102', '14104', '14110', '14114', '14118', '14121' +] diff --git a/src/interfaces/account.ts b/src/interfaces/account.ts index ebfcc508..fcec6495 100644 --- a/src/interfaces/account.ts +++ b/src/interfaces/account.ts @@ -4,7 +4,8 @@ export enum AccountFacet { Genshin = 'genshin', - StarRail = 'starrail' + StarRail = 'starrail', + ZenlessZoneZero = 'zzz' } export interface KnownAccountProperties { @@ -30,13 +31,20 @@ export function resolveAccountDisplayName (facet: AccountFacet, account: Account } // TODO: Default display name i18n - return account?.properties?.displayName || ( - facet === AccountFacet.Genshin - ? '旅行者' - : facet === AccountFacet.StarRail - ? '开拓者' - : 'NULL' - ) + if (account?.properties?.displayName) { + return account.properties.displayName + } else { + switch (facet) { + case AccountFacet.Genshin: + return '旅行者' + case AccountFacet.StarRail: + return '开拓者' + case AccountFacet.ZenlessZoneZero: + return '绳匠' + default: + return 'NULL' + } + } } // TODO: i18n @@ -46,6 +54,8 @@ export function resolveCurrency (facet: AccountFacet): { currency: string, actio return { currency: '原石', action: '祈愿' } case AccountFacet.StarRail: return { currency: '星琼', action: '跃迁' } + case AccountFacet.ZenlessZoneZero: + return { currency: '菲林', action: '调频' } default: throw new Error(`Unknown account facet: ${facet}`) } diff --git a/src/interfaces/gacha.ts b/src/interfaces/gacha.ts index 697fd59c..7ea8b078 100644 --- a/src/interfaces/gacha.ts +++ b/src/interfaces/gacha.ts @@ -28,3 +28,18 @@ export interface StarRailGachaRecord { item_type: string // 角色 | 光锥 rank_type: string // 3 | 4 | 5 } + +// See: src-tauri/src/gacha/impl_zzz.rs +export interface ZenlessZoneZeroGachaRecord { + id: string + uid: string + gacha_id: string + gacha_type: string // 1 | 2 | 3 | 4 | 5 + item_id: string + count: string // always 1 + time: string + name: string + lang: string // zh-cn + item_type: string // 代理人 | 音擎 + rank_type: string // 2 | 3 | 4 +} diff --git a/src/router.tsx b/src/router.tsx index bf9c68d9..0b074ddb 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -6,6 +6,7 @@ import Root from '@/routes/root' import Index from '@/routes/index' import Genshin, { loader as genshinLoader } from '@/routes/genshin' import StarRail, { loader as starrailLoader } from '@/routes/starrail' +import ZenlessZoneZero, { loader as zzzLoader } from '@/routes/zzz' import Setting from '@/routes/setting' const router = createBrowserRouter([ @@ -25,6 +26,11 @@ const router = createBrowserRouter([ element: , loader: starrailLoader(queryClient) }, + { + path: '/zzz', + element: , + loader: zzzLoader(queryClient) + }, { path: '/setting', element: diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 0e42f7ff..bf5330af 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -76,7 +76,7 @@ export default function Index () { }}> 功能
    -
  • 支持 原神崩坏:星穹铁道 游戏抽卡记录。
  • +
  • 支持 原神崩坏:星穹铁道绝区零 游戏抽卡记录。
  • 管理游戏的多个账号。
  • 获取游戏的抽卡链接。
  • @@ -111,8 +111,6 @@ export default function Index () {
      {[ ['https://uigf.org/', 'UIGF organization'], - ['https://github.com/DGP-Studio/Snap.Hutao', 'DGP-Studio/Snap.Hutao'], - ['https://github.com/YuehaiTeam/cocogoat', 'YuehaiTeam/cocogoat'], ['https://github.com/vikiboss/gs-helper', 'vikiboss/gs-helper'] ].map(([href, title], i) => (
    • diff --git a/src/routes/zzz.tsx b/src/routes/zzz.tsx new file mode 100644 index 00000000..dba9a774 --- /dev/null +++ b/src/routes/zzz.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { AccountFacet } from '@/interfaces/account' +import { createStatefulAccountLoader, withStatefulAccount } from '@/hooks/useStatefulAccount' +import Layout from '@/components/Layout' +import AccountMenu from '@/components/account/AccountMenu' +import GachaLayout from '@/components/gacha/GachaLayout' + +export const loader = createStatefulAccountLoader(AccountFacet.ZenlessZoneZero) + +export default withStatefulAccount(AccountFacet.ZenlessZoneZero, function ZenlessZoneZero () { + return ( + }> + + + ) +}) diff --git a/src/utilities/plugin-gacha.ts b/src/utilities/plugin-gacha.ts index dbf0537c..da4c0bf6 100644 --- a/src/utilities/plugin-gacha.ts +++ b/src/utilities/plugin-gacha.ts @@ -1,5 +1,5 @@ import { Account, AccountFacet } from '@/interfaces/account' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' +import { GenshinGachaRecord, StarRailGachaRecord, ZenlessZoneZeroGachaRecord } from '@/interfaces/gacha' import invoke from '@/utilities/invoke' export async function findGameDataDirectories (facet: AccountFacet): Promise { @@ -20,8 +20,8 @@ export async function pullAllGachaRecords ( payload: { gachaUrl: string gachaTypeAndLastEndIdMappings: Record< - GenshinGachaRecord['gacha_type'] | StarRailGachaRecord['gacha_type'], - GenshinGachaRecord['id'] | StarRailGachaRecord['id'] | null + GenshinGachaRecord['gacha_type'] | StarRailGachaRecord['gacha_type'] | ZenlessZoneZeroGachaRecord['gacha_type'], + GenshinGachaRecord['id'] | StarRailGachaRecord['id'] | ZenlessZoneZeroGachaRecord['id'] | null > eventChannel: string saveToStorage?: boolean diff --git a/src/utilities/plugin-storage.ts b/src/utilities/plugin-storage.ts index 60979a29..6a909982 100644 --- a/src/utilities/plugin-storage.ts +++ b/src/utilities/plugin-storage.ts @@ -1,5 +1,5 @@ import { AccountFacet, Account } from '@/interfaces/account' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' +import { GenshinGachaRecord, StarRailGachaRecord, ZenlessZoneZeroGachaRecord } from '@/interfaces/gacha' import invoke from '@/utilities/invoke' export type AccountUid = Account['uid'] @@ -7,7 +7,7 @@ export type CreateAccountPayload = Omit export type FindGachaRecordsPayload = { uid: AccountUid - gachaType?: GenshinGachaRecord['gacha_type'] | StarRailGachaRecord['gacha_type'] + gachaType?: GenshinGachaRecord['gacha_type'] | StarRailGachaRecord['gacha_type'] | ZenlessZoneZeroGachaRecord['gacha_type'] limit?: number } @@ -54,29 +54,13 @@ export async function deleteAccount (facet: AccountFacet, uid: AccountUid): Prom export async function findGachaRecords ( facet: AccountFacet, payload: FindGachaRecordsPayload -): Promise> -export async function findGachaRecords ( - facet: AccountFacet, - payload: FindGachaRecordsPayload -): Promise> -export async function findGachaRecords ( - facet: AccountFacet, - payload: FindGachaRecordsPayload -): Promise> { +): Promise> { return invoke(`plugin:storage|find_${facet}_gacha_records`, payload) } -export async function saveGachaRecords ( - facet: AccountFacet.Genshin, - records: Array -): Promise -export async function saveGachaRecords ( - facet: AccountFacet.StarRail, - records: Array -): Promise export async function saveGachaRecords ( facet: AccountFacet, - records: Array + records: Array ): Promise { return invoke(`plugin:storage|save_${facet}_gacha_records`, { records }) }