diff --git a/.changeset/calm-coats-tell.md b/.changeset/calm-coats-tell.md new file mode 100644 index 0000000..77df402 --- /dev/null +++ b/.changeset/calm-coats-tell.md @@ -0,0 +1,5 @@ +--- +"ganymede-app": minor +--- + +Vous pouvez désormais visualiser l'Almanax sur plusieurs jours. L'affichage de l'Almanax a légèrement changé. diff --git a/.changeset/chilly-snails-drive.md b/.changeset/chilly-snails-drive.md new file mode 100644 index 0000000..2273f79 --- /dev/null +++ b/.changeset/chilly-snails-drive.md @@ -0,0 +1,5 @@ +--- +"ganymede-app": minor +--- + +Vous pouvez désormais choisir votre niveau lors du calcul des récompenses de l'Almanax. diff --git a/.changeset/sixty-buckets-begin.md b/.changeset/sixty-buckets-begin.md new file mode 100644 index 0000000..ca6cf4f --- /dev/null +++ b/.changeset/sixty-buckets-begin.md @@ -0,0 +1,5 @@ +--- +"ganymede-app": minor +--- + +L'Almanax prend désormais en charge les différents fuseaux horaires. Les serveurs étant tous basés sur le fuseau horaire Europe/Paris, l'application prend donc également cette direction. diff --git a/package.json b/package.json index d8dec80..f43a137 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "country-flag-icons": "^1.5.14", + "dayjs": "^1.11.13", "html-react-parser": "^5.2.2", "lucide-react": "^0.474.0", "neverthrow": "^8.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 722f582..269a884 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: country-flag-icons: specifier: ^1.5.14 version: 1.5.14 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 html-react-parser: specifier: ^5.2.2 version: 5.2.2(@types/react@19.0.8)(react@19.0.0) @@ -2113,6 +2116,9 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -5003,6 +5009,8 @@ snapshots: date-fns@3.6.0: {} + dayjs@1.11.13: {} + debug@4.4.0: dependencies: ms: 2.1.3 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b0a8145..8ee9fd6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -660,6 +660,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.11.2", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen 0.11.3", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -1499,6 +1520,7 @@ name = "ganymede" version = "1.7.1" dependencies = [ "chrono", + "chrono-tz", "glob", "log", "machine-uid", @@ -3140,6 +3162,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -3208,6 +3239,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + [[package]] name = "phf_generator" version = "0.8.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9ac655d..0e0a99a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,6 +43,7 @@ tauri-plugin-log = "2.2.0" log = "^0.4" taurpc = { path = "./taurpc", version = "0.3.2" } tauri-plugin-opener = "2.2.1" +chrono-tz = "0.10.1" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-window-state = "2.2.0" diff --git a/src-tauri/src/almanax.rs b/src-tauri/src/almanax.rs index 70f4e43..7120ce7 100644 --- a/src-tauri/src/almanax.rs +++ b/src-tauri/src/almanax.rs @@ -1,7 +1,8 @@ use crate::api::DOFUSDB_API; use crate::item::Item; use crate::quest::get_quest_data; -use chrono::Datelike; +use chrono::prelude::*; +use chrono_tz::Europe::Paris; use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri_plugin_http::reqwest; @@ -10,7 +11,6 @@ use crate::conf::{Conf, ConfLang}; const REWARD_REDUCED_SCALE: f32 = 0.7; const REWARD_SCALE_CAP: f32 = 1.5; -const PLAYER_LEVEL: u32 = 200; #[derive(Debug, Serialize, thiserror::Error)] pub enum Error { @@ -32,34 +32,6 @@ pub enum Error { Quest(#[from] crate::quest::Error), } -// impl Into for Error { -// fn into(self) -> tauri::ipc::InvokeError { -// match self { -// Error::DofusDbAlmanaxMalformed(err) => tauri::ipc::InvokeError::from(format!( -// "DofusDbAlmanaxMalformed({})", -// err.to_string() -// )), -// Error::DofusDbItemMalformed(err) => { -// tauri::ipc::InvokeError::from(format!("DofusDbItemMalformed({})", err.to_string())) -// } -// Error::RequestAlmanax(err) => { -// tauri::ipc::InvokeError::from(format!("RequestAlmanax({})", err.to_string())) -// } -// Error::RequestAlmanaxContent(err) => { -// tauri::ipc::InvokeError::from(format!("RequestAlmanaxContent({})", err.to_string())) -// } -// Error::RequestItem(err) => { -// tauri::ipc::InvokeError::from(format!("RequestItem({})", err.to_string())) -// } -// Error::RequestItemContent(err) => { -// tauri::ipc::InvokeError::from(format!("RequestItemContent({})", err.to_string())) -// } -// Error::Conf(err) => err.into(), -// Error::Quest(err) => err.into(), -// } -// } -// } - #[derive(Serialize, Deserialize, Debug)] pub struct AlmanaxName { en: String, @@ -160,8 +132,10 @@ pub fn get_experience_reward( } } -pub async fn get_almanax_data() -> Result { - let date = chrono::offset::Local::now(); +pub async fn get_almanax_data(date: String) -> Result { + let date = DateTime::parse_from_rfc3339(date.as_str()) + .unwrap() + .with_timezone(&Paris); let day = date.day(); let month = date.month(); let year = date.year(); @@ -200,7 +174,7 @@ pub async fn get_item_data(item_id: u32) -> Result { #[taurpc::procedures(path = "almanax", export_to = "../src/ipc/bindings.ts")] pub trait AlmanaxApi { - async fn get(app_handle: AppHandle) -> Result; + async fn get(app_handle: AppHandle, level: u32, date: String) -> Result; } #[derive(Clone)] @@ -208,8 +182,8 @@ pub struct AlmanaxApiImpl; #[taurpc::resolvers] impl AlmanaxApi for AlmanaxApiImpl { - async fn get(self, app: AppHandle) -> Result { - let almanax = get_almanax_data().await?; + async fn get(self, app: AppHandle, level: u32, date: String) -> Result { + let almanax = get_almanax_data(date).await?; let quest = get_quest_data(almanax.id).await.map_err(Error::Quest)?; let item_id = quest.data[0].steps[0].objectives[0].need.generated.items[0]; let quantity = quest.data[0].steps[0].objectives[0] @@ -227,14 +201,14 @@ impl AlmanaxApi for AlmanaxApiImpl { }; let experience = get_experience_reward( - PLAYER_LEVEL, + level, quest.optimal_level(), quest.experience_ratio(), quest.duration(), ); let kamas = get_kamas_reward( - PLAYER_LEVEL, + level, quest.level_max(), quest.optimal_level(), quest.kamas_ratio(), diff --git a/src-tauri/src/conf.rs b/src-tauri/src/conf.rs index 5f6dd07..1e348f3 100644 --- a/src-tauri/src/conf.rs +++ b/src-tauri/src/conf.rs @@ -6,6 +6,12 @@ use std::collections::HashMap; use std::fs; use tauri::{AppHandle, Manager, Window, Wry}; +const DEFAULT_LEVEL: u32 = 200; + +const fn default_level() -> u32 { + DEFAULT_LEVEL +} + #[derive(Debug, Serialize, thiserror::Error)] pub enum Error { #[error("failed to get conf, file is malformed")] @@ -26,29 +32,12 @@ pub enum Error { ResetConf(Box), } -// impl Into for Error { -// fn into(self) -> InvokeError { -// use Error::*; - -// let message = match self { -// Malformed(err) => format!("Malformed({})", err.to_string()), -// CreateConfDir(err) => format!("CreateConfDir({})", err.to_string()), -// ConfDir(err) => format!("ConfDir({})", err.to_string()), -// SerializeConf(err) => format!("SerializeConf({})", err.to_string()), -// UnhandledIo(err) => format!("UnhandledIo({})", err.to_string()), -// SaveConf(err) => format!("SaveConf({})", err.to_string()), -// GetProfileInUse => "GetProfileInUse".to_string(), -// ResetConf(err) => return (*err).into(), -// }; - -// InvokeError::from(message) -// } -// } - #[derive(Serialize, Deserialize, Debug, Clone, taurpc::specta::Type)] pub struct Profile { pub id: String, pub name: String, + #[serde(default = "default_level")] + pub level: u32, pub progresses: Vec, } @@ -243,6 +232,7 @@ impl Default for Profile { Profile { id: uuid::Uuid::new_v4().to_string(), name: "Player".to_string(), + level: 200, progresses: vec![], } } diff --git a/src/components/almanax-frame.tsx b/src/components/almanax-frame.tsx index b45f9f5..b6c2796 100644 --- a/src/components/almanax-frame.tsx +++ b/src/components/almanax-frame.tsx @@ -1,51 +1,143 @@ import kamasIcon from '@/assets/kamas.webp' import xpIcon from '@/assets/xp.webp' +import { Button } from '@/components/ui/button.tsx' +import { useProfile } from '@/hooks/use_profile.ts' import { getLang } from '@/lib/conf.ts' -import { almanaxQuery } from '@/queries/almanax.query' -import { confQuery } from '@/queries/conf.query' +import { useSetConf } from '@/mutations/set-conf.mutation.ts' +import { almanaxQuery } from '@/queries/almanax.query.ts' +import { confQuery } from '@/queries/conf.query.ts' +import { Trans } from '@lingui/react/macro' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' -import { LoaderIcon } from 'lucide-react' -import { DownloadImage } from './download-image' +import { type Dayjs } from 'dayjs' +import { ChevronLeftIcon, ChevronRightIcon, LoaderIcon } from 'lucide-react' +import { useState } from 'react' +import { newDateFromParis } from '../lib/date.ts' +import { DownloadImage } from './download-image.tsx' +import { InvisibleInput } from './invisible-input.tsx' + +function dateToDayMonthYear(date: Dayjs) { + if (date.hour() === 0) { + return date.format('DD/MM/YYYY') + } + + return date.format('DD/MM/YYYY HH:mm') +} + +const defaultLevel = 200 export function AlmanaxFrame() { + const [date, setDate] = useState(newDateFromParis()) + const setConf = useSetConf() const conf = useSuspenseQuery(confQuery) - const almanax = useQuery(almanaxQuery(getLang(conf.data.lang))) + const profile = useProfile() + const [almanaxLevel, setAlmanaxLevel] = useState((profile.level ?? defaultLevel).toString()) + const almanax = useQuery(almanaxQuery(getLang(conf.data.lang), profile.level ?? defaultLevel, date)) + + const onPreviousDay = () => { + setDate(date.subtract(1, 'day')) + } + const onNextDay = () => { + setDate(date.add(1, 'day')) + } + const onGoToday = () => { + setDate(newDateFromParis()) + } return ( -
- {almanax.isLoading && } - {almanax.isSuccess && ( -
-
-
Almanax
- Lvl: 200 + <> +
+ + +
+ {dateToDayMonthYear(date)} + +
+
+
+
+
+ + Lvl:{' '} + { + if (value === '') { + setAlmanaxLevel((profile.level ?? defaultLevel).toString()) + return + } + + const level = parseInt(value) + + if (value === '' || Number.isNaN(level)) { + return + } + + setConf.mutate({ + ...conf.data, + profiles: conf.data.profiles.map((p) => { + if (p.id === conf.data.profileInUse) { + return { + ...p, + level, + } + } + return p + }), + }) + setAlmanaxLevel(level.toString()) + }} + /> +
-
- {almanax.data.img && ( - - )} -
-
- {almanax.data.quantity.toLocaleString()}x{' '} - - {almanax.data.name} - -
-
-
- - {almanax.data.experience.toLocaleString()} -
-
- - {almanax.data.kamas.toLocaleString()} + {almanax.isLoading && ( +
+ +
+ )} + {almanax.isSuccess && ( + <> +
+ {almanax.data.img && ( + + )} +
+
+ {almanax.data.quantity.toLocaleString()}x{' '} + + {almanax.data.name} + +
+
+
+ + {almanax.data.experience.toLocaleString()} +
+
+ + {almanax.data.kamas.toLocaleString()} +
+
-
-
-
+
+ + )}
- )} -
+
+ ) } diff --git a/src/components/change-step.tsx b/src/components/change-step.tsx index 0809ab2..1553b12 100644 --- a/src/components/change-step.tsx +++ b/src/components/change-step.tsx @@ -1,7 +1,8 @@ import { Button } from '@/components/ui/button.tsx' import { useWebviewEvent } from '@/hooks/use_webview_event' import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' -import { useEffect, useRef, useState } from 'react' +import { useState } from 'react' +import { InvisibleInput } from './invisible-input.tsx' export function ChangeStep({ currentIndex, @@ -18,36 +19,23 @@ export function ChangeStep({ }) { const current = currentIndex + 1 const [innerValue, setInnerValue] = useState(current.toString()) - const [hadLostFocus, setHadLostFocus] = useState(true) - const formRef = useRef(null) const onInnerNext = async () => { const canMove = await onNext() - if (!canMove) { return } - setInnerValue((current + 1).toString()) } const onInnerPrevious = async () => { const canMove = await onPrevious() - if (!canMove) { return } - setInnerValue((current - 1).toString()) } - // biome-ignore lint/correctness/useExhaustiveDependencies: no need for innerValue - useEffect(() => { - if (innerValue !== current.toString()) { - setInnerValue(current.toString()) - } - }, [current]) - useWebviewEvent( 'go-to-previous-guide-step', async () => { @@ -64,76 +52,24 @@ export function ChangeStep({ [current], ) - const handleChange = async () => { - if (!formRef.current) { - return - } - - const data = new FormData(formRef.current) - const value = data.get('current') as string - - if (value === '') { - return - } - - const number = parseInt(value) - - if (Number.isNaN(number) || number < 1) { - return - } - - const numberIndex = number - 1 - const nextStepIndex = numberIndex > maxIndex ? maxIndex : numberIndex - - await setCurrentIndex(nextStepIndex) - - setInnerValue((nextStepIndex + 1).toString()) - } - return (
-
{ - evt.preventDefault() - - await handleChange() + { + const numberIndex = parseInt(value) + await setCurrentIndex(numberIndex) + setInnerValue((numberIndex + 1).toString()) }} - > - { - const value = evt.currentTarget.value - - setInnerValue(value) - }} - className="w-8 bg-transparent text-center text-xs outline-hidden" - onBlur={async () => { - setHadLostFocus(true) - - await handleChange() - }} - onClick={(evt) => { - const input = evt.currentTarget - - if (hadLostFocus) { - input.select() - setHadLostFocus(false) - } - }} - autoCapitalize="off" - autoCorrect="off" - autoComplete="off" - max={maxIndex + 1} - min={1} - /> - + /> {maxIndex + 1}