diff --git a/Foundation/Statistics.cs b/Foundation/Statistics.cs new file mode 100644 index 0000000..2406bf4 --- /dev/null +++ b/Foundation/Statistics.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using DynamicData; +using DynamicData.Kernel; +using iTimeSlot.Models; + +public interface IStatistics +{ + DailyStat ReadTodayData(); + void ReadWeekData(); + + void CompleteTask(int minute); + void CompleteBreak(int minute); +} + + +//DiskStatistics is a class that implements IStatistics interface using disk as storage +public class DiskStatistics : IStatistics +{ + private string iTimeslotDateFormat ="yyyy-MM-dd"; //date format: 2024-06-01 + private string _dataPath = ""; + + public DiskStatistics(string dataPath) + { + _dataPath = dataPath; + } + + private void EnsureExist() + { + if (string.IsNullOrEmpty(_dataPath)) + { + throw new Exception("Data path is not set"); + } + var parentDir = Path.GetDirectoryName(_dataPath); + if (string.IsNullOrEmpty(parentDir)) + throw new Exception("Invalid Parent directory"); + + if (!Directory.Exists(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + if (!File.Exists(_dataPath)) + { + File.WriteAllText(_dataPath, "{}"); + } + } + + public void CompleteBreak(int minute) + { + EnsureExist(); + LogFinishedInterval(IntervalType.Break, minute); + } + + public void CompleteTask(int minute) + { + EnsureExist(); + LogFinishedInterval(IntervalType.Work, minute); + } + + + + private void LogFinishedInterval(IntervalType type, int minute) + { + //read from disk and update + try + { + string jsonString = File.ReadAllText(_dataPath); + var stats = JsonSerializer.Deserialize(jsonString, new JsonContext().Stats); + if (stats == null) + { + stats = new Stats() + { + DailyStats = new List() { } + }; + } + else if (stats.DailyStats == null) + { + stats.DailyStats = new List() { }; + } + + var entry = new DailyStat() + { + Date = DateTime.Today.ToString(iTimeslotDateFormat), + }; + + var existed = stats.DailyStats.FirstOrDefault(s => s.Date == DateTime.Today.ToString(iTimeslotDateFormat)); + if (existed != null) + { + entry = existed; + } + + //update entry + if (type == IntervalType.Work) + { + entry.WorkCount += 1; + entry.TotalWorkMinutes += minute; + } + else + { + entry.BreakCount += 1; + entry.TotalBreakMinutes += minute; + } + + //add to list if not existed + if (existed == null) + { + stats.DailyStats.Add(entry); + } + + var json = JsonSerializer.Serialize(stats, new JsonContext().Stats); + File.WriteAllText(_dataPath, json); + } + catch (Exception ex) + { + Console.WriteLine("Failed to update db:" + ex.Message); + } + + } + + public DailyStat ReadTodayData() + { + EnsureExist(); + string jsonString = File.ReadAllText(_dataPath); + var stats = JsonSerializer.Deserialize(jsonString, new JsonContext().Stats); + + var ds = new DailyStat() + { + Date = DateTime.Today.ToString(iTimeslotDateFormat), + }; + + if (stats == null || stats.DailyStats == null) + { + return ds; + } + + var existed = stats.DailyStats.FirstOrDefault(s => s.Date == DateTime.Today.ToString(iTimeslotDateFormat)); + if (existed != null) + { + ds = existed; + } + + return ds; + } + + public void ReadWeekData() + { + EnsureExist(); + throw new NotImplementedException(); + } +} diff --git a/Models/JsonContext.cs b/Models/JsonContext.cs index 3a389ba..2cefe83 100644 --- a/Models/JsonContext.cs +++ b/Models/JsonContext.cs @@ -1,11 +1,12 @@ -using System; -using System.Text.Json.Serialization; - -namespace iTimeSlot.Models; - -[JsonSerializable(typeof(Settings))] -[JsonSerializable(typeof(bool))] -[JsonSerializable(typeof(int))] -[JsonSerializable(typeof(TimeSpan))] -[JsonSerializable(typeof(TimeSlot))] +using System; +using System.Text.Json.Serialization; + +namespace iTimeSlot.Models; + +[JsonSerializable(typeof(Stats))] +[JsonSerializable(typeof(Settings))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(TimeSpan))] +[JsonSerializable(typeof(TimeSlot))] internal partial class JsonContext : JsonSerializerContext {} \ No newline at end of file diff --git a/Models/Stat.cs b/Models/Stat.cs new file mode 100644 index 0000000..881a274 --- /dev/null +++ b/Models/Stat.cs @@ -0,0 +1,27 @@ + + +using System; +using System.Collections.Generic; + +public class DailyStat +{ + public string Date { get; set; } + + //WorkCount is the number of work intervals completed + public int WorkCount { get; set; } + + //TotalWorkMinutes is the total minutes spent on work + public int TotalWorkMinutes { get; set; } + + //BreakCount is the number of break intervals completed + public int BreakCount { get; set; } + + //TotalBreakMinutes is the total minutes spent on break + public int TotalBreakMinutes { get; set; } + +} + +public class Stats +{ + public List DailyStats { get; set; } +} \ No newline at end of file diff --git a/Models/TimeSlot.cs b/Models/TimeSlot.cs index 30f7012..0e0397a 100644 --- a/Models/TimeSlot.cs +++ b/Models/TimeSlot.cs @@ -1,57 +1,65 @@ -using System; -using System.Text.Json.Serialization; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace iTimeSlot.Models -{ - public class TimeSlot : ObservableObject - { - public bool IsSystemPreserved { get; set; } - - private TimeSpan _ts; - - [JsonPropertyName("TimeSpan")] - public TimeSpan Ts - { - get { return _ts; } - set { _ts = value; } - } - - - //keep this public parameterless constructor for json serialization and deserialization - [JsonConstructor] - public TimeSlot() - { - - } - - public TimeSlot(TimeSpan srcTs, bool isSystemPreserved = false) - { - this._ts = srcTs; - IsSystemPreserved = isSystemPreserved; - } - - public TimeSlot(int minute, bool isSystemPreserved = false) : this(TimeSpan.FromMinutes(minute), isSystemPreserved) - { - - } - - public override string ToString() - { - int m = (int)_ts.TotalMinutes; - string unit = m >1? "mins" : "min"; - return $"{(int)_ts.TotalMinutes} {unit}".TrimEnd(); - } - - public TimeSpan ToTimeSpan() - { - return _ts; - } - - public int TotalSeconds() - { - return (int)_ts.TotalSeconds; - } - - } -} +using System; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace iTimeSlot.Models +{ + public class TimeSlot : ObservableObject + { + public bool IsSystemPreserved { get; set; } + public IntervalType IntervalType { get; set; } + + private TimeSpan _ts; + + [JsonPropertyName("TimeSpan")] + public TimeSpan Ts + { + get { return _ts; } + set { _ts = value; } + } + + + //keep this public parameterless constructor for json serialization and deserialization + [JsonConstructor] + public TimeSlot() + { + + } + + public TimeSlot(TimeSpan srcTs, IntervalType iType, bool isSystemPreserved = false) + { + this._ts = srcTs; + IsSystemPreserved = isSystemPreserved; + IntervalType = iType; + } + + public TimeSlot(int minute, IntervalType iType, bool isSystemPreserved = false) : + this(TimeSpan.FromMinutes(minute),iType, isSystemPreserved) + { + + } + + public override string ToString() + { + int m = (int)_ts.TotalMinutes; + string unit = m > 1 ? "mins" : "min"; + return $"{(int)_ts.TotalMinutes} {unit}".TrimEnd(); + } + + public TimeSpan ToTimeSpan() + { + return _ts; + } + + public int TotalSeconds() + { + return (int)_ts.TotalSeconds; + } + + } + public enum IntervalType + { + Work, + Break + } +} diff --git a/Shared/DefaultConfig.cs b/Shared/DefaultConfig.cs index be5c4c1..cf3bb84 100644 --- a/Shared/DefaultConfig.cs +++ b/Shared/DefaultConfig.cs @@ -1,29 +1,31 @@ -using System; -using System.Collections.Generic; - -namespace iTimeSlot.Shared -{ - - public static class DefaultConfig - { - public static string[] RemindBefores = new string[] - { - "1 minute before", - "2 minute before", - "5 minute before", - "10 minute before" - }; - - - public static readonly List SysTimeSlots = new List{ - TimeSpan.FromMinutes(5), - TimeSpan.FromMinutes(25), - TimeSpan.FromMinutes(60) - }; - - public static readonly bool SysCloseWithoutExit = true; - public static readonly bool SysPlaySound = false; - public static readonly bool SysShowProgInTray = true; - } - +using System; +using System.Collections.Generic; + +namespace iTimeSlot.Shared +{ + + public static class DefaultConfig + { + public static string[] RemindBefores = new string[] + { + "1 minute before", + "2 minute before", + "5 minute before", + "10 minute before" + }; + + + public static readonly List SysWorkTimeSlots = new List{ + TimeSpan.FromMinutes(25), + TimeSpan.FromMinutes(60) + }; + public static readonly List SysBreakTimeSlots = new List{ + TimeSpan.FromMinutes(5), + }; + + public static readonly bool SysCloseWithoutExit = true; + public static readonly bool SysPlaySound = false; + public static readonly bool SysShowProgInTray = true; + } + } \ No newline at end of file diff --git a/Shared/Timer.cs b/Shared/Timer.cs index 3024797..b999dce 100644 --- a/Shared/Timer.cs +++ b/Shared/Timer.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using iTimeSlot.Models; namespace iTimeSlot.Shared { @@ -20,8 +21,9 @@ public class Timer private bool _isStarted = false; + private IntervalType _invervalType = IntervalType.Work; - public void Init(DateTime startTime, TimeSpan duration, + public void Init(IntervalType type, DateTime startTime, TimeSpan duration, OnProgressUpdateDelegate onProgressUpdateFunc, OnTimeUpDelegate onTimeupFunc) { this.Stop(); @@ -31,6 +33,7 @@ public void Init(DateTime startTime, TimeSpan duration, //Console.WriteLine("start time: " + StartTime, "Duration: " + Duration, "End time: " + EndTime); _progressCallback = onProgressUpdateFunc; _timeupCallback = onTimeupFunc; + _invervalType = type; } public bool IsTimeUp() @@ -42,6 +45,11 @@ public bool IsStarted() return _isStarted; } + public bool IsWorkInterval() + { + return _invervalType == IntervalType.Work; + } + public bool Start() { if (_isStarted) diff --git a/Shared/Vars.cs b/Shared/Vars.cs index 2adc4c5..ea9e2d9 100644 --- a/Shared/Vars.cs +++ b/Shared/Vars.cs @@ -1,42 +1,50 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using iTimeSlot.Models; - - -namespace iTimeSlot.Shared -{ - internal static class Global - { - public static Timer MyTimer = new Timer(); - - public static Settings LoaddedSetting = new (); - - public static string ConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".iTimeSlot", "config.json"); - - public static Settings EnsureDefaultConfigFile() - { - var defSetting = new Settings() - { - CloseWithoutExit = DefaultConfig.SysCloseWithoutExit, - LastUsedIndex = 0, - PlaySound = DefaultConfig.SysPlaySound, - ShowProgressInTry = DefaultConfig.SysShowProgInTray - }; - - defSetting.TimeSlots = new List(); - foreach (var st in DefaultConfig.SysTimeSlots) - { - defSetting.TimeSlots.Add(new TimeSlot(st,true)); - } - - defSetting.SaveToDisk(ConfigPath); - return defSetting; - } - } - - } \ No newline at end of file +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using iTimeSlot.Models; + + +namespace iTimeSlot.Shared +{ + internal static class Global + { + public static Timer MyTimer = new Timer(); + + public static Settings LoaddedSetting = new(); + + public static string ConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".iTimeSlot", "config.json"); + private static string statPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".iTimeSlot", "stats.json"); + public static IStatistics StatReporter = new DiskStatistics(statPath); + + public static Settings EnsureDefaultConfigFile() + { + var defSetting = new Settings() + { + CloseWithoutExit = DefaultConfig.SysCloseWithoutExit, + LastUsedIndex = 0, + PlaySound = DefaultConfig.SysPlaySound, + ShowProgressInTry = DefaultConfig.SysShowProgInTray + }; + + defSetting.TimeSlots = new List(); + foreach (var st in DefaultConfig.SysWorkTimeSlots) + { + defSetting.TimeSlots.Add(new TimeSlot(st, IntervalType.Work, true)); + } + foreach (var st in DefaultConfig.SysBreakTimeSlots) + { + defSetting.TimeSlots.Add(new TimeSlot(st, IntervalType.Break, true)); + } + + defSetting.SaveToDisk(ConfigPath); + return defSetting; + } + } + +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 661c50b..2f215a6 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -25,6 +25,12 @@ public partial class MainWindowViewModel : ObservableViewModelBase public MainWindowViewModel() { _trayHelper = new TrayHelper(); + + //init latest data + var data = Global.StatReporter.ReadTodayData(); + TotalWorkMinutes = data.TotalWorkMinutes; + TotalBreakMinutes = data.TotalBreakMinutes; + CompletedWorkTimers = data.WorkCount; } private ObservableCollection _slots; public ObservableCollection AllTimeSlots @@ -75,6 +81,34 @@ public bool ProgressVisible set { this.SetProperty(ref _progressVisible, value); } } + private bool _isWorkIntervalSelected = true; + public bool IsWorkIntervalSelected + { + get { return _isWorkIntervalSelected; } + set { this.SetProperty(ref _isWorkIntervalSelected, value); } + } + + private int _totalWorkMinutes; + public int TotalWorkMinutes + { + get { return _totalWorkMinutes; } + set { this.SetProperty(ref _totalWorkMinutes, value); } + } + + private int _totalBreakMinutes; + public int TotalBreakMinutes + { + get { return _totalBreakMinutes; } + set { this.SetProperty(ref _totalBreakMinutes, value); } + } + + private int _completedWorkTimers; + public int CompletedWorkTimers + { + get { return _completedWorkTimers; } + set { this.SetProperty(ref _completedWorkTimers, value); } + } + private bool _isTimeSlotComboBoxEnabled = true; public bool IsTimeSlotComboBoxEnabled { @@ -144,7 +178,7 @@ public void DeleteTimeSpan(TimeSlot toDel) public void AddTimeWindow() { - + //AddTimeDialog use the same context //We set close action here so that other method that bind to the View can call it directly. For instance, AddTimeSpan() AddTimeDialog addTimeDiag = new AddTimeDialog(this); @@ -155,7 +189,8 @@ public void AddTimeWindow() addTimeDiag.BringIntoView(); addTimeDiag.ShowDialog(mainWindow); } - + + //addTimeSpanOkAction is the action after adding timespan, such as closing the dialog private event Action addTimeSpanOkAction; public void AddTimeSpan(decimal? toAdd) @@ -177,7 +212,8 @@ public void AddTimeSpan(decimal? toAdd) return; } } - AllTimeSlots.Add(new TimeSlot(toAddInt, false)); + IntervalType t = IsWorkIntervalSelected ? IntervalType.Work : IntervalType.Break; + AllTimeSlots.Add(new TimeSlot(toAddInt, t, false)); TimeToAdd = null;//reset the view SyncSettings(); @@ -245,7 +281,7 @@ public void StartCmd() var tm = Global.MyTimer; var selected = AllTimeSlots[IndexOfSelectedTimeInWorkspace]; - tm.Init(DateTime.Now, selected.ToTimeSpan(), ProgressUpdateAction, TimeupAction); + tm.Init(selected.IntervalType, DateTime.Now, selected.ToTimeSpan(), ProgressUpdateAction, TimeupAction); //update to full before start which will be reset to 0 ProgressValue = 100; @@ -301,6 +337,24 @@ private async void TimeupAction() { await Dispatcher.UIThread.Invoke(async () => { + //a working timer done + //accumulate the total work time + //write completed timer + if (Global.MyTimer.IsWorkInterval()) + { + Global.StatReporter.CompleteTask(Global.MyTimer.Duration.Minutes); + } + else + { + //a break timer done + //accumulate the total break time + Global.StatReporter.CompleteBreak(Global.MyTimer.Duration.Minutes); + } + //read latest data + var data = Global.StatReporter.ReadTodayData(); + TotalWorkMinutes = data.TotalWorkMinutes; + TotalBreakMinutes = data.TotalBreakMinutes; + CompletedWorkTimers = data.WorkCount; if (PlaySound) { diff --git a/Views/AddTimeDialog.axaml b/Views/AddTimeDialog.axaml index 2fd66b3..d4d69ff 100644 --- a/Views/AddTimeDialog.axaml +++ b/Views/AddTimeDialog.axaml @@ -44,7 +44,7 @@ diff --git a/Views/WorkspaceTab.axaml b/Views/WorkspaceTab.axaml index ac1eb39..1fc9c5c 100644 --- a/Views/WorkspaceTab.axaml +++ b/Views/WorkspaceTab.axaml @@ -3,47 +3,55 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:iTimeSlot.ViewModels" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" x:DataType="vm:MainWindowViewModel" x:Class="iTimeSlot.Views.WorkspaceTab"> - - + - - + - - + + - - - + + - + - - - - + - + + + + + + + + + + + + + + + + + + + + + + + - - + \ No newline at end of file