Documentation
This commit is contained in:
@@ -11,16 +11,23 @@ use std::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Duration (ms) an informational status message remains visible.
|
||||
const STATUS_DURATION_MS: i64 = 4000;
|
||||
/// Amount of time (ms) an alarm is considered to be "ringing" once triggered.
|
||||
const RINGING_DURATION_MS: i64 = 6000;
|
||||
/// Minimum timer duration accepted from the UI to avoid accidental zero timers.
|
||||
const TIMER_MIN_MS: u64 = 1000;
|
||||
|
||||
/// Shared state container held by Tauri. It wraps a mutex-protected
|
||||
/// [`StateInner`] and is responsible for persistence plus providing immutable
|
||||
/// snapshots to the frontend.
|
||||
pub struct AppState {
|
||||
inner: Mutex<StateInner>,
|
||||
storage_path: PathBuf,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Constructs a new state container and eagerly loads persisted alarms.
|
||||
pub fn new(storage_path: PathBuf) -> Self {
|
||||
let mut inner = StateInner::default();
|
||||
if let Some(list) = load_alarms(&storage_path) {
|
||||
@@ -44,56 +51,69 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `FrontendState` without mutating anything. Primarily used when
|
||||
/// the UI first boots.
|
||||
pub fn snapshot(&self) -> FrontendState {
|
||||
trace!("Building snapshot for frontend");
|
||||
self.apply(|_, _, _| {})
|
||||
}
|
||||
|
||||
/// Executes the housekeeping refresh logic and returns an updated snapshot.
|
||||
pub fn tick(&self) -> FrontendState {
|
||||
trace!("Processing tick request");
|
||||
self.apply(|_, _, _| {})
|
||||
}
|
||||
|
||||
/// Creates a new alarm entry using the provided time and label.
|
||||
pub fn create_alarm(&self, time: String, label: Option<String>) -> FrontendState {
|
||||
self.apply(|inner, now_ms, now_local| {
|
||||
inner.create_alarm(&time, label.as_deref(), now_ms, now_local);
|
||||
})
|
||||
}
|
||||
|
||||
/// Toggles an existing alarm's active state.
|
||||
pub fn toggle_alarm(&self, id: String) -> FrontendState {
|
||||
self.apply(|inner, _now_ms, now_local| {
|
||||
inner.toggle_alarm(&id, now_local);
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes an alarm permanently.
|
||||
pub fn delete_alarm(&self, id: String) -> FrontendState {
|
||||
self.apply(|inner, now_ms, _| {
|
||||
inner.delete_alarm(&id, now_ms);
|
||||
})
|
||||
}
|
||||
|
||||
/// Starts or pauses the timer depending on the current state.
|
||||
pub fn toggle_timer(&self, input: Option<String>, force_parse: bool) -> FrontendState {
|
||||
self.apply(|inner, now_ms, _| {
|
||||
inner.toggle_timer(input.as_deref(), force_parse, now_ms);
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the timer to its initial duration.
|
||||
pub fn reset_timer(&self) -> FrontendState {
|
||||
self.apply(|inner, _, _| inner.reset_timer())
|
||||
}
|
||||
|
||||
/// Starts or pauses the stopwatch.
|
||||
pub fn toggle_stopwatch(&self) -> FrontendState {
|
||||
self.apply(|inner, now_ms, _| inner.toggle_stopwatch(now_ms))
|
||||
}
|
||||
|
||||
/// Records a lap for the stopwatch if running.
|
||||
pub fn add_lap(&self) -> FrontendState {
|
||||
self.apply(|inner, now_ms, _| inner.add_lap(now_ms))
|
||||
}
|
||||
|
||||
/// Resets the stopwatch to zero elapsed time and clears laps.
|
||||
pub fn reset_stopwatch(&self) -> FrontendState {
|
||||
self.apply(|inner, _, _| inner.reset_stopwatch())
|
||||
}
|
||||
|
||||
/// Executes a state mutation closure while handling locking, refresh logic,
|
||||
/// snapshot generation, and persistence side effects.
|
||||
fn apply<F>(&self, mutator: F) -> FrontendState
|
||||
where
|
||||
F: FnOnce(&mut StateInner, i64, &DateTime<Local>),
|
||||
@@ -125,6 +145,7 @@ impl AppState {
|
||||
snapshot
|
||||
}
|
||||
|
||||
/// Persists the alarm list to disk, creating directories as needed.
|
||||
fn save_alarms(&self, alarms: &[PersistedAlarm]) {
|
||||
if let Some(parent) = self.storage_path.parent() {
|
||||
if let Err(error) = fs::create_dir_all(parent) {
|
||||
@@ -161,6 +182,7 @@ impl AppState {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// Mutable inner state containing alarms, timers, and transient UI messaging.
|
||||
struct StateInner {
|
||||
alarms: Vec<Alarm>,
|
||||
timer: TimerState,
|
||||
@@ -171,12 +193,14 @@ struct StateInner {
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
/// Executes periodic tasks that should run on every mutation or tick.
|
||||
fn refresh(&mut self, now_ms: i64, now_local: &DateTime<Local>) {
|
||||
self.process_alarms(now_ms, now_local);
|
||||
self.update_timer(now_ms);
|
||||
self.expire_status(now_ms);
|
||||
}
|
||||
|
||||
/// Builds a full [`FrontendState`] representation of the current data.
|
||||
fn to_frontend(&mut self, now_local: &DateTime<Local>) -> FrontendState {
|
||||
let now_ms = now_local.timestamp_millis();
|
||||
let now_utc = now_local.with_timezone(&Utc);
|
||||
@@ -201,6 +225,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new alarm and schedules its next trigger time.
|
||||
fn create_alarm(
|
||||
&mut self,
|
||||
raw_time: &str,
|
||||
@@ -235,6 +260,7 @@ impl StateInner {
|
||||
self.push_status(&format!("アラーム追加: {}", label_text), now_ms);
|
||||
}
|
||||
|
||||
/// Toggles an alarm's active state and recalculates its next trigger.
|
||||
fn toggle_alarm(&mut self, id: &str, now_local: &DateTime<Local>) {
|
||||
if let Some(alarm) = self.alarms.iter_mut().find(|alarm| alarm.id == id) {
|
||||
alarm.active = !alarm.active;
|
||||
@@ -251,6 +277,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes an alarm completely.
|
||||
fn delete_alarm(&mut self, id: &str, now_ms: i64) {
|
||||
if let Some(index) = self.alarms.iter().position(|alarm| alarm.id == id) {
|
||||
let removed = self.alarms.remove(index);
|
||||
@@ -262,6 +289,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles timer start/pause logic, parsing inputs when necessary.
|
||||
fn toggle_timer(&mut self, input: Option<&str>, force_parse: bool, now_ms: i64) {
|
||||
if self.timer.running {
|
||||
if let Some(target) = self.timer.target_epoch_ms {
|
||||
@@ -317,6 +345,7 @@ impl StateInner {
|
||||
);
|
||||
}
|
||||
|
||||
/// Restores the timer to its initial duration without starting it.
|
||||
fn reset_timer(&mut self) {
|
||||
self.timer.running = false;
|
||||
self.timer.target_epoch_ms = None;
|
||||
@@ -324,6 +353,7 @@ impl StateInner {
|
||||
info!("Timer reset to {}ms", self.timer.initial_ms);
|
||||
}
|
||||
|
||||
/// Starts or pauses the stopwatch while accounting for elapsed time.
|
||||
fn toggle_stopwatch(&mut self, now_ms: i64) {
|
||||
if self.stopwatch.running {
|
||||
if let Some(start) = self.stopwatch.start_epoch_ms {
|
||||
@@ -340,6 +370,8 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a lap entry capturing both the total elapsed time and the delta
|
||||
/// since the previous lap.
|
||||
fn add_lap(&mut self, now_ms: i64) {
|
||||
let elapsed = self.stopwatch_elapsed(now_ms);
|
||||
if elapsed == 0 {
|
||||
@@ -369,6 +401,7 @@ impl StateInner {
|
||||
);
|
||||
}
|
||||
|
||||
/// Stops the stopwatch and clears any lap history.
|
||||
fn reset_stopwatch(&mut self) {
|
||||
self.stopwatch.running = false;
|
||||
self.stopwatch.start_epoch_ms = None;
|
||||
@@ -377,6 +410,8 @@ impl StateInner {
|
||||
info!("Stopwatch reset");
|
||||
}
|
||||
|
||||
/// Iterates configured alarms, triggering those scheduled for the current
|
||||
/// tick and scheduling their next activation.
|
||||
fn process_alarms(&mut self, now_ms: i64, now_local: &DateTime<Local>) {
|
||||
let mut triggered_labels = Vec::new();
|
||||
for alarm in &mut self.alarms {
|
||||
@@ -408,6 +443,8 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates timer countdown bookkeeping and fires completion status when the
|
||||
/// clock hits zero.
|
||||
fn update_timer(&mut self, now_ms: i64) {
|
||||
if self.timer.running {
|
||||
if let Some(target) = self.timer.target_epoch_ms {
|
||||
@@ -425,6 +462,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Expires the active status message once its TTL elapses.
|
||||
fn expire_status(&mut self, now_ms: i64) {
|
||||
if let Some(status) = &self.status {
|
||||
if status.expires_at <= now_ms {
|
||||
@@ -433,6 +471,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the alarm section view model that the frontend expects.
|
||||
fn build_alarm_view(&self, now_ms: i64) -> AlarmSection {
|
||||
let mut sorted = self.alarms.iter().collect::<Vec<_>>();
|
||||
sorted.sort_by_key(|alarm| alarm.next_trigger);
|
||||
@@ -471,6 +510,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the timer view model by formatting durations and button labels.
|
||||
fn build_timer_view(&self) -> TimerView {
|
||||
let display_ms = if self.timer.running {
|
||||
self.timer.remaining_ms
|
||||
@@ -502,6 +542,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the stopwatch view model including lap breakdowns.
|
||||
fn build_stopwatch_view(&self, now_ms: i64) -> StopwatchView {
|
||||
let elapsed = self.stopwatch_elapsed(now_ms);
|
||||
let toggle_label = if self.stopwatch.running {
|
||||
@@ -517,6 +558,8 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total stopwatch elapsed milliseconds, optionally including
|
||||
/// the in-flight span if the stopwatch is currently running.
|
||||
fn stopwatch_elapsed(&self, now_ms: i64) -> u64 {
|
||||
self.stopwatch.elapsed_before_ms
|
||||
+ if self.stopwatch.running {
|
||||
@@ -526,6 +569,7 @@ impl StateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a transient status message that the UI can render for users.
|
||||
fn push_status(&mut self, message: &str, now_ms: i64) {
|
||||
self.status = Some(StatusMessage {
|
||||
text: message.to_string(),
|
||||
@@ -575,6 +619,7 @@ struct StatusMessage {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Comprehensive state payload consumed by the frontend.
|
||||
pub struct FrontendState {
|
||||
pub clock: ClockState,
|
||||
pub alarms: AlarmSection,
|
||||
@@ -585,6 +630,7 @@ pub struct FrontendState {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Clock label strings rendered in the UI.
|
||||
pub struct ClockState {
|
||||
pub local_time: String,
|
||||
pub local_date: String,
|
||||
@@ -593,12 +639,14 @@ pub struct ClockState {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Aggregated alarm listing plus small summary for the next trigger.
|
||||
pub struct AlarmSection {
|
||||
pub items: Vec<AlarmView>,
|
||||
pub next_summary: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Individual alarm entry displayed in the frontend list.
|
||||
pub struct AlarmView {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
@@ -609,6 +657,7 @@ pub struct AlarmView {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Timer labels consumed by the UI controls.
|
||||
pub struct TimerView {
|
||||
pub display: String,
|
||||
pub toggle_label: String,
|
||||
@@ -616,6 +665,7 @@ pub struct TimerView {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Stopwatch value for the UI including lap breakdowns.
|
||||
pub struct StopwatchView {
|
||||
pub display: String,
|
||||
pub toggle_label: String,
|
||||
@@ -623,6 +673,7 @@ pub struct StopwatchView {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Single lap renderable information.
|
||||
pub struct LapView {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
@@ -631,6 +682,7 @@ pub struct LapView {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
/// Minimal alarm representation stored on disk.
|
||||
struct PersistedAlarm {
|
||||
id: String,
|
||||
hour: u8,
|
||||
@@ -640,6 +692,7 @@ struct PersistedAlarm {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
/// Wrapper used when serializing alarm lists to JSON.
|
||||
struct AlarmStore {
|
||||
alarms: Vec<PersistedAlarm>,
|
||||
}
|
||||
@@ -656,6 +709,7 @@ impl From<&Alarm> for PersistedAlarm {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to load the alarm store JSON file from disk.
|
||||
fn load_alarms(path: &Path) -> Option<Vec<PersistedAlarm>> {
|
||||
match fs::read(path) {
|
||||
Ok(bytes) => match serde_json::from_slice::<AlarmStore>(&bytes) {
|
||||
@@ -678,18 +732,23 @@ fn load_alarms(path: &Path) -> Option<Vec<PersistedAlarm>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a timestamp as HH:MM:SS.
|
||||
fn format_time(hour: u32, minute: u32, second: u32) -> String {
|
||||
format!("{:02}:{:02}:{:02}", hour, minute, second)
|
||||
}
|
||||
|
||||
/// Formats a date as YYYY-MM-DD.
|
||||
fn format_date(year: i32, month: u32, day: u32) -> String {
|
||||
format!("{year:04}-{month:02}-{day:02}")
|
||||
}
|
||||
|
||||
/// Formats an alarm display label.
|
||||
fn format_time_label(hour: u8, minute: u8) -> String {
|
||||
format!("{:02}:{:02}", hour, minute)
|
||||
}
|
||||
|
||||
/// Produces a human-friendly relative time string describing when an alarm
|
||||
/// will next trigger.
|
||||
fn format_relative(target_ms: i64, now_ms: i64) -> String {
|
||||
let diff = target_ms - now_ms;
|
||||
if diff <= 0 {
|
||||
@@ -707,6 +766,7 @@ fn format_relative(target_ms: i64, now_ms: i64) -> String {
|
||||
format!("{}時間{}分後", hours, mins)
|
||||
}
|
||||
|
||||
/// Formats a duration for timer display, switching to HH:MM:SS when needed.
|
||||
fn format_duration(ms: u64) -> String {
|
||||
let total_seconds = (ms / 1000) as u64;
|
||||
let minutes = total_seconds / 60;
|
||||
@@ -721,6 +781,7 @@ fn format_duration(ms: u64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats stopwatch durations with millisecond precision.
|
||||
fn format_stopwatch(ms: u64) -> String {
|
||||
let total_seconds = ms / 1000;
|
||||
let minutes = total_seconds / 60;
|
||||
@@ -729,6 +790,7 @@ fn format_stopwatch(ms: u64) -> String {
|
||||
format!("{:02}:{:02}.{:03}", minutes, seconds, millis)
|
||||
}
|
||||
|
||||
/// Converts raw lap entries into renderable view data.
|
||||
fn build_lap_views(entries: &[LapEntry]) -> Vec<LapView> {
|
||||
if entries.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -747,6 +809,7 @@ fn build_lap_views(entries: &[LapEntry]) -> Vec<LapView> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses HH:MM strings emitted by the UI.
|
||||
fn parse_alarm_time(value: &str) -> Option<(u8, u8)> {
|
||||
let trimmed = value.trim();
|
||||
let mut parts = trimmed.split(':');
|
||||
@@ -758,6 +821,7 @@ fn parse_alarm_time(value: &str) -> Option<(u8, u8)> {
|
||||
Some((hour, minute))
|
||||
}
|
||||
|
||||
/// Parses timer duration input supporting seconds, MM:SS, or `1h 30m` formats.
|
||||
fn parse_duration(value: &str) -> Option<u64> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -778,6 +842,7 @@ fn parse_duration(value: &str) -> Option<u64> {
|
||||
parse_unit_duration(trimmed)
|
||||
}
|
||||
|
||||
/// Helper that parses unit-suffixed duration tokens like `10m 5s`.
|
||||
fn parse_unit_duration(value: &str) -> Option<u64> {
|
||||
let mut total_ms = 0u64;
|
||||
for chunk in value.split_whitespace() {
|
||||
@@ -805,6 +870,7 @@ fn parse_unit_duration(value: &str) -> Option<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the next trigger timestamp in milliseconds for an alarm.
|
||||
fn resolve_next_trigger(hour: u8, minute: u8, now_local: &DateTime<Local>) -> i64 {
|
||||
let naive_time = NaiveTime::from_hms_opt(hour as u32, minute as u32, 0)
|
||||
.unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
|
||||
@@ -816,6 +882,8 @@ fn resolve_next_trigger(hour: u8, minute: u8, now_local: &DateTime<Local>) -> i6
|
||||
candidate.timestamp_millis()
|
||||
}
|
||||
|
||||
/// Safely combines a date and time within the Local timezone, falling back to
|
||||
/// UTC if the local conversion is ambiguous or invalid.
|
||||
fn combine_local(date: NaiveDate, time: NaiveTime) -> DateTime<Local> {
|
||||
let naive = NaiveDateTime::new(date, time);
|
||||
match Local.from_local_datetime(&naive) {
|
||||
|
||||
Reference in New Issue
Block a user