Build1=Default,b4a.example File1=Main_Layout.bal FileGroup1=Default Group Group=Default Group Library1=core Library2=xui Library3=javaobject ManifestCode='This code will be applied to the manifest file during compilation.~\n~'You do not need to modify it in most cases.~\n~'See this link for for more information: https://www.b4x.com/forum/showthread.php?p=78136~\n~AddManifestText(~\n~~\n~)~\n~SetApplicationAttribute(android:icon, "@drawable/icon")~\n~SetApplicationAttribute(android:label, "$LABEL$")~\n~CreateResourceFromFile(Macro, Themes.LightTheme)~\n~'End of default text.~\n~ Module1=AnalogClock Module2=Starter Module3=Localization NumberOfFiles=1 NumberOfLibraries=3 NumberOfModules=3 Version=13 @EndOfDesignText@ #Region Project Attributes #ApplicationLabel: Overtime Guard #VersionCode: 1 #VersionName: 1.0 #CanInstallToExternalStorage: False #End Region #Region Activity Attributes #FullScreen: False #IncludeTitle: False #End Region Sub Process_Globals Private Timer1 As Timer Private FirstWorkLimitMs As Long Private WorkLimitMs As Long Private OvertimeLimitMs As Long Private MaxProductiveMs As Long Private StatsFileName As String Private StateFileName As String Private AutoConfigFileName As String Private SettingsFileName As String Private MaxStoredDays As Int End Sub Sub Globals Private BtnStart As Button Private BtnPause As Button Private BtnReset As Button Private BtnSync As Button Private BtnStats As Label Private BtnStatsClose As Button Private BtnConfig As Label Private BtnConfigClose As Button Private BtnConfigSave As Button Private BtnConfigReset As Button Private BtnClearToday As Button Private BtnHelpOpen As Button Private BtnHelpClose As Button Private chkIncludePauses As CheckBox Private BtnBackground As Label Private lblAppName As Label Private lblBrandInfo As Label Private lblElapsedTime As Label Private lblAutoStatus As Label Private pnlClock As Panel Private pnlStats As Panel Private pnlConfig As Panel Private pnlHelp As Panel Private svStats As ScrollView Private svHelp As ScrollView Private txtCfgStart As EditText Private txtCfgPauseStart As EditText Private txtCfgPauseEnd As EditText Private txtCfgEnd As EditText Private myClock As AnalogClock Private SessionActive As Boolean Private PauseActive As Boolean Private ProductiveElapsedMs As Long Private CurrentSegmentStart As Long Private CurrentSegmentColor As Int Private StatsRows As List Private WorkMorningColor As Int Private CurrentWorkDayStart As Long Private CurrentWorkDayKey As Long Private AutoModeEnabled As Boolean Private AutoStartMinutes As Int Private AutoPauseStartMinutes As Int Private AutoPauseEndMinutes As Int Private AutoEndMinutes As Int Private AutoState As String Private LastActiveStateSaveAt As Long Private IncludePauseInTotal As Boolean End Sub Sub Activity_Create(FirstTime As Boolean) Localization.Initialize Activity.LoadLayout("Main_Layout") Activity.Title = Localization.T("app_title") Log("Layout loaded successfully.") ResizeClockPanel FirstWorkLimitMs = 4 * DateTime.TicksPerHour WorkLimitMs = 8 * DateTime.TicksPerHour OvertimeLimitMs = DateTime.TicksPerHour MaxProductiveMs = WorkLimitMs + OvertimeLimitMs WorkMorningColor = Colors.RGB(0, 190, 255) StatsFileName = "work_segments.txt" StateFileName = "active_state.txt" AutoConfigFileName = "auto_config.txt" SettingsFileName = "app_settings.txt" MaxStoredDays = 62 IncludePauseInTotal = True myClock.Initialize(pnlClock, 96) myClock.SetBaseDuration(12 * DateTime.TicksPerHour) Timer1.Initialize("Timer1", 1000) Timer1.Enabled = True StatsRows.Initialize LoadStatsRows LoadSettings HideLegacyButtons CreateStatsButton CreateStatsPage CreateConfigButton CreateConfigPage CreateHelpPage CreateBackgroundButton CreateAppNameLabel CreateBrandInfoLabel CreateAutoStatusLabel LayoutMainButtons LoadAutoConfig ResetSessionState(False) If AutoModeEnabled Then UpdateAutomaticState(DateTime.Now) Else RestoreActiveState If SessionActive = False Then RestoreLastClosedDayState End If End Sub Sub Timer1_Tick If AutoModeEnabled Then UpdateAutomaticState(DateTime.Now) Return End If If SessionActive Then Dim now As Long = DateTime.Now If PauseActive Then ProcessActivePauseSegment(now) If SessionActive = False Then Return myClock.SetActiveSegment(now - CurrentSegmentStart, Colors.Yellow, True) Else ProcessActiveWorkSegment(now) If SessionActive Then myClock.SetActiveSegment(now - CurrentSegmentStart, CurrentSegmentColor, False) End If End If If SessionActive And now - LastActiveStateSaveAt >= 30 * DateTime.TicksPerSecond Then SaveActiveState UpdateElapsedLabel Else myClock.DrawClock End If End Sub Sub Activity_Resume If AutoModeEnabled Then UpdateAutomaticState(DateTime.Now) Else RestoreActiveState If SessionActive = False Then RestoreLastClosedDayState End If End Sub Private Sub ResizeClockPanel Dim clockSize As Int = Activity.Width pnlClock.SetLayout(0, 0, clockSize, clockSize) End Sub Private Sub CreateStatsButton BtnStats.Initialize("BtnStats") BtnStats.Text = Localization.T("stats") Activity.AddView(BtnStats, BtnReset.Left, BtnReset.Top, BtnReset.Width, BtnReset.Height) End Sub Private Sub CreateStatsPage pnlStats.Initialize("") pnlStats.Color = Colors.White pnlStats.Visible = False Activity.AddView(pnlStats, 0, 0, Activity.Width, Activity.Height) Dim title As Label title.Initialize("") title.Text = Localization.T("statistics") title.TextSize = 22 title.TextColor = Colors.Black title.Gravity = Gravity.CENTER_VERTICAL pnlStats.AddView(title, 12dip, 8dip, Activity.Width - 110dip, 46dip) BtnStatsClose.Initialize("BtnStatsClose") BtnStatsClose.Text = Localization.T("close") pnlStats.AddView(BtnStatsClose, Activity.Width - 94dip, 8dip, 84dip, 46dip) svStats.Initialize(0) pnlStats.AddView(svStats, 0, 62dip, Activity.Width, Activity.Height - 62dip) End Sub Private Sub CreateConfigButton BtnConfig.Initialize("BtnConfig") BtnConfig.Text = Localization.T("config") Activity.AddView(BtnConfig, BtnReset.Left, BtnReset.Top, BtnReset.Width, BtnReset.Height) End Sub Private Sub CreateBackgroundButton BtnBackground.Initialize("BtnBackground") BtnBackground.Text = Localization.T("bg") Activity.AddView(BtnBackground, BtnPause.Left, BtnPause.Top, BtnPause.Width, BtnPause.Height) End Sub Private Sub CreateAppNameLabel lblAppName.Initialize("") lblAppName.Text = Localization.T("app_title") lblAppName.TextColor = Colors.Black lblAppName.TextSize = 22 lblAppName.Typeface = Typeface.CreateNew(Typeface.SANS_SERIF, Typeface.STYLE_BOLD_ITALIC) lblAppName.Gravity = Gravity.CENTER Activity.AddView(lblAppName, 0, 0, 10dip, 10dip) End Sub Private Sub CreateBrandInfoLabel lblBrandInfo.Initialize("") lblBrandInfo.Text = "AI Team Studio | aiteamstudio.it" lblBrandInfo.TextColor = Colors.RGB(90, 90, 90) lblBrandInfo.TextSize = 11 lblBrandInfo.Gravity = Gravity.CENTER Activity.AddView(lblBrandInfo, 0, 0, 10dip, 10dip) End Sub Private Sub LayoutMainButtons Dim margin As Int = 8dip Dim gap As Int = 8dip Dim rowGap As Int = 8dip Dim buttonHeight As Int = 48dip Dim labelTop As Int = Activity.Height - 28dip Dim row2Top As Int = labelTop - gap - buttonHeight Dim row1Top As Int = row2Top - rowGap - buttonHeight Dim wideButtonWidth As Int = (Activity.Width - (2 * margin) - gap) / 2 Dim smallButtonWidth As Int = (Activity.Width - (2 * margin) - (2 * gap)) / 3 BtnStart.TextSize = 16 BtnPause.TextSize = 16 BtnBackground.TextSize = 15 BtnStats.TextSize = 15 BtnConfig.TextSize = 15 StyleNeutralButton(BtnPause) StyleNeutralButton(BtnBackground) StyleNeutralButton(BtnStats) StyleNeutralButton(BtnConfig) BtnStart.SetLayout(margin, row1Top, wideButtonWidth, buttonHeight) BtnPause.SetLayout(margin + wideButtonWidth + gap, row1Top, wideButtonWidth, buttonHeight) BtnBackground.SetLayout(margin, row2Top, smallButtonWidth, buttonHeight) BtnStats.SetLayout(margin + smallButtonWidth + gap, row2Top, smallButtonWidth, buttonHeight) BtnConfig.SetLayout(margin + (2 * (smallButtonWidth + gap)), row2Top, smallButtonWidth, buttonHeight) If lblAppName.IsInitialized Then Dim titleTop As Int = pnlClock.Top + pnlClock.Height + 4dip Dim titleHeight As Int = Max(28dip, row1Top - titleTop - 4dip) Dim appNameHeight As Int = Max(24dip, titleHeight * 0.58) lblAppName.SetLayout(0, titleTop, Activity.Width, appNameHeight) If lblBrandInfo.IsInitialized Then lblBrandInfo.SetLayout(0, titleTop + appNameHeight - 2dip, Activity.Width, Max(18dip, titleHeight - appNameHeight + 2dip)) End If If lblElapsedTime.IsInitialized Then lblElapsedTime.SetLayout(0, labelTop, Activity.Width * 0.58, 28dip) If lblAutoStatus.IsInitialized Then lblAutoStatus.SetLayout(Activity.Width * 0.58, labelTop, Activity.Width * 0.42, 28dip) End Sub Private Sub StyleNeutralButton(btn As Label) If btn.IsInitialized = False Then Return btn.Color = Colors.RGB(230, 230, 230) btn.TextColor = Colors.Black btn.Gravity = Gravity.CENTER End Sub Private Sub CreateAutoStatusLabel lblAutoStatus.Initialize("") lblAutoStatus.TextColor = Colors.DarkGray lblAutoStatus.TextSize = 13 lblAutoStatus.Gravity = Bit.Or(Gravity.RIGHT, Gravity.CENTER_VERTICAL) lblAutoStatus.Text = Localization.T("manual_mode") Activity.AddView(lblAutoStatus, 0, 0, 10dip, 10dip) End Sub Private Sub CreateConfigPage pnlConfig.Initialize("") pnlConfig.Color = Colors.White pnlConfig.Visible = False Activity.AddView(pnlConfig, 0, 0, Activity.Width, Activity.Height) Dim title As Label title.Initialize("") title.Text = Localization.T("config") title.TextSize = 22 title.TextColor = Colors.Black title.Gravity = Gravity.CENTER_VERTICAL pnlConfig.AddView(title, 12dip, 8dip, Activity.Width - 110dip, 46dip) BtnConfigClose.Initialize("BtnConfigClose") BtnConfigClose.Text = Localization.T("close") pnlConfig.AddView(BtnConfigClose, Activity.Width - 94dip, 8dip, 84dip, 46dip) Dim y As Int = 78dip txtCfgStart = AddConfigField(Localization.T("work_start"), "08:30", y) y = y + 58dip txtCfgPauseStart = AddConfigField(Localization.T("pause_start"), "12:30", y) y = y + 58dip txtCfgPauseEnd = AddConfigField(Localization.T("pause_end"), "13:00", y) y = y + 58dip txtCfgEnd = AddConfigField(Localization.T("work_end"), "17:00", y) y = y + 58dip chkIncludePauses.Initialize("chkIncludePauses") chkIncludePauses.Text = Localization.T("include_pause_total") chkIncludePauses.TextSize = 15 chkIncludePauses.Checked = IncludePauseInTotal pnlConfig.AddView(chkIncludePauses, 12dip, y, Activity.Width - 24dip, 48dip) BtnConfigSave.Initialize("BtnConfigSave") BtnConfigSave.Text = Localization.T("set_config") pnlConfig.AddView(BtnConfigSave, 12dip, y + 56dip, (Activity.Width - 32dip) / 2, 48dip) BtnConfigReset.Initialize("BtnConfigReset") BtnConfigReset.Text = Localization.T("reset_config") pnlConfig.AddView(BtnConfigReset, 20dip + ((Activity.Width - 32dip) / 2), y + 56dip, (Activity.Width - 32dip) / 2, 48dip) BtnClearToday.Initialize("BtnClearToday") BtnClearToday.Text = Localization.T("clear_today") pnlConfig.AddView(BtnClearToday, 12dip, y + 112dip, Activity.Width - 24dip, 48dip) BtnHelpOpen.Initialize("BtnHelpOpen") BtnHelpOpen.Text = Localization.T("help") pnlConfig.AddView(BtnHelpOpen, 12dip, y + 168dip, Activity.Width - 24dip, 48dip) End Sub Private Sub CreateHelpPage pnlHelp.Initialize("") pnlHelp.Color = Colors.White pnlHelp.Visible = False Activity.AddView(pnlHelp, 0, 0, Activity.Width, Activity.Height) Dim title As Label title.Initialize("") title.Text = Localization.T("help") title.TextSize = 22 title.TextColor = Colors.Black title.Gravity = Gravity.CENTER_VERTICAL pnlHelp.AddView(title, 12dip, 8dip, Activity.Width - 110dip, 46dip) BtnHelpClose.Initialize("BtnHelpClose") BtnHelpClose.Text = Localization.T("close") pnlHelp.AddView(BtnHelpClose, Activity.Width - 94dip, 8dip, 84dip, 46dip) svHelp.Initialize(0) pnlHelp.AddView(svHelp, 0, 62dip, Activity.Width, Activity.Height - 62dip) Dim lblHelp As Label lblHelp.Initialize("") lblHelp.Text = BuildHelpText lblHelp.TextSize = 16 lblHelp.TextColor = Colors.Black lblHelp.Gravity = Gravity.TOP svHelp.Panel.AddView(lblHelp, 12dip, 12dip, Activity.Width - 24dip, 1180dip) svHelp.Panel.Height = 1204dip End Sub Private Sub AddConfigField(LabelText As String, ValueText As String, y As Int) As EditText Dim lbl As Label lbl.Initialize("") lbl.Text = LabelText lbl.TextSize = 16 lbl.TextColor = Colors.Black lbl.Gravity = Gravity.CENTER_VERTICAL pnlConfig.AddView(lbl, 12dip, y, 140dip, 46dip) Dim txt As EditText txt.Initialize("") txt.Text = ValueText txt.TextSize = 18 txt.SingleLine = True txt.InputType = txt.INPUT_TYPE_NUMBERS pnlConfig.AddView(txt, 160dip, y, Activity.Width - 172dip, 46dip) Return txt End Sub Sub Activity_Pause (UserClosed As Boolean) If AutoModeEnabled = False Then SaveActiveState End Sub ' Formatta il tempo in HH:MM:SS Sub FormatElapsedTime(ms As Long) As String Dim seconds As Int = ms / 1000 Dim minutes As Int = seconds / 60 Dim hours As Int = minutes / 60 seconds = seconds Mod 60 minutes = minutes Mod 60 Return NumberFormat(hours, 2, 0) & ":" & NumberFormat(minutes, 2, 0) & ":" & NumberFormat(seconds, 2, 0) End Sub ' Pulsante Start Sub BtnStart_Click If AutoModeEnabled Then Return If SessionActive = False Then StartSession Else EndSession(DateTime.Now, False) End If End Sub ' Pulsante Pause Sub BtnPause_Click If AutoModeEnabled Then Return If SessionActive = False Then Return Dim now As Long = DateTime.Now If PauseActive Then CloseCurrentSegment(now) PauseActive = False BtnPause.Text = Localization.T("pause") CurrentSegmentStart = now CurrentSegmentColor = GetCurrentWorkColor myClock.SetActiveSegment(0, CurrentSegmentColor, False) SaveActiveState Log("Pause ended.") Else ProcessActiveWorkSegment(now) If SessionActive = False Then Return CloseCurrentSegment(now) PauseActive = True BtnPause.Text = Localization.T("end_pause") CurrentSegmentStart = now CurrentSegmentColor = Colors.Yellow myClock.SetActiveSegment(0, CurrentSegmentColor, True) SaveActiveState Log("Pause started.") End If End Sub ' Pulsante Reset Sub BtnReset_Click ShowStatsPage End Sub ' Pulsante Sync Sub BtnSync_Click ShowConfigPage End Sub Sub BtnStats_Click ShowStatsPage End Sub Sub BtnStatsClose_Click ShowMainPage End Sub Sub BtnConfig_Click ShowConfigPage End Sub Sub BtnConfigClose_Click ShowMainPage End Sub Sub BtnConfigSave_Click SetAutomaticConfig End Sub Sub BtnConfigReset_Click ResetAutomaticConfig End Sub Sub chkIncludePauses_CheckedChange(Checked As Boolean) IncludePauseInTotal = Checked SaveSettings ApplyPauseCountingMode End Sub Sub BtnClearToday_Click ClearDisplayedDay End Sub Sub BtnHelpOpen_Click ShowHelpPage End Sub Sub BtnHelpClose_Click ShowConfigPage End Sub Sub BtnBackground_Click SaveActiveState Dim home As Intent home.Initialize("android.intent.action.MAIN", "") home.AddCategory("android.intent.category.HOME") home.Flags = 268435456 StartActivity(home) End Sub ' Sincronizza l'orologio Sub SynchronizeClock myClock.DrawClock End Sub Private Sub StartSession CurrentWorkDayStart = GetWorkDayStart(DateTime.Now) CurrentWorkDayKey = CurrentWorkDayStart ProductiveElapsedMs = GetProductiveTotalForWorkDay(CurrentWorkDayKey) If ProductiveElapsedMs >= MaxProductiveMs Then ToastMessageShow(Localization.T("limit_reached"), False) UpdateElapsedLabel Log("Start ignored: 9-hour limit already reached.") Return End If SessionActive = True PauseActive = False CurrentSegmentStart = DateTime.Now CurrentSegmentColor = GetCurrentWorkColor BtnStart.Text = Localization.T("end") BtnPause.Text = Localization.T("pause") BtnPause.Enabled = True UpdateMainControlState myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.SetActiveSegment(0, CurrentSegmentColor, False) UpdateElapsedLabel SaveActiveState Log("Workday started.") End Sub Private Sub EndSession(FinalTime As Long, Automatic As Boolean) If SessionActive Then CloseCurrentSegment(FinalTime) SessionActive = False PauseActive = False BtnStart.Text = Localization.T("start") BtnPause.Text = Localization.T("pause") BtnPause.Enabled = False UpdateMainControlState myClock.ClearActiveSegment UpdateElapsedLabel If File.Exists(File.DirInternal, StateFileName) Then File.Delete(File.DirInternal, StateFileName) If Automatic Then Log("Workday stopped automatically.") Else Log("Workday closed manually.") End If End Sub Private Sub ResetSessionState(ClearClock As Boolean) SessionActive = False PauseActive = False ProductiveElapsedMs = 0 CurrentSegmentStart = 0 CurrentWorkDayStart = 0 CurrentWorkDayKey = 0 CurrentSegmentColor = WorkMorningColor LastActiveStateSaveAt = 0 BtnStart.Text = Localization.T("start") BtnPause.Text = Localization.T("pause") BtnPause.Enabled = False UpdateMainControlState If ClearClock Then myClock.ClearSegments myClock.ClearActiveSegment UpdateElapsedLabel End Sub Private Sub ProcessActiveWorkSegment(now As Long) Do While SessionActive And PauseActive = False Dim midnight As Long = GetNextMidnight(CurrentSegmentStart) If now >= midnight Then EndSession(midnight, True) Log("Workday stopped automatically: midnight reached.") Return End If Dim segmentDuration As Long = now - CurrentSegmentStart If segmentDuration <= 0 Then Return Dim nextBoundary As Long = GetNextWorkBoundary Dim durationToBoundary As Long = nextBoundary - ProductiveElapsedMs If segmentDuration < durationToBoundary Then CurrentSegmentColor = GetCurrentWorkColor Return End If RecordSegment(CurrentSegmentStart, durationToBoundary, GetCurrentWorkColor, False) ProductiveElapsedMs = ProductiveElapsedMs + durationToBoundary CurrentSegmentStart = CurrentSegmentStart + durationToBoundary CurrentSegmentColor = GetCurrentWorkColor If ProductiveElapsedMs >= MaxProductiveMs Then EndSession(CurrentSegmentStart, True) Return End If Loop End Sub Private Sub CloseCurrentSegment(FinalTime As Long) Dim duration As Long = FinalTime - CurrentSegmentStart If duration <= 0 Then Return If PauseActive Then RecordSegment(CurrentSegmentStart, duration, Colors.Yellow, True) If IncludePauseInTotal Then ProductiveElapsedMs = ProductiveElapsedMs + duration Else RecordSegment(CurrentSegmentStart, duration, GetCurrentWorkColor, False) ProductiveElapsedMs = ProductiveElapsedMs + duration End If End Sub Private Sub RecordSegment(SegmentStart As Long, Duration As Long, SegmentColor As Int, IsPauseSegment As Boolean) myClock.AddSegment(Duration, SegmentColor, IsPauseSegment) AddStatsSegment(CurrentWorkDayStart, CurrentWorkDayKey, SegmentStart, Duration, GetSegmentCategory(SegmentColor, IsPauseSegment), "manual") End Sub Private Sub AddStatsSegment(DayStart As Long, DayKey As Long, SegmentStart As Long, Duration As Long, Category As String, Source As String) Dim row As Map row.Initialize row.Put("dayStart", DayStart) row.Put("dayKey", DayKey) row.Put("segmentStart", SegmentStart) row.Put("date", DateTime.Date(DayStart)) row.Put("duration", Duration) row.Put("category", Category) row.Put("source", Source) StatsRows.Add(row) TrimStatsRows SaveStatsRows End Sub Private Sub ProcessActivePauseSegment(now As Long) If IncludePauseInTotal Then Dim remainingToLimit As Long = MaxProductiveMs - ProductiveElapsedMs If remainingToLimit <= 0 Then EndSession(CurrentSegmentStart, True) Log("Workday stopped automatically during pause: 9-hour limit reached.") Return End If Dim limitTime As Long = CurrentSegmentStart + remainingToLimit If limitTime <= GetNextMidnight(CurrentSegmentStart) And now >= limitTime Then EndSession(limitTime, True) Log("Workday stopped automatically during pause: 9-hour limit reached.") Return End If End If Dim midnight As Long = GetNextMidnight(CurrentSegmentStart) If now >= midnight Then EndSession(midnight, True) Log("Workday stopped automatically during pause: midnight reached.") End If End Sub Private Sub GetSegmentCategory(SegmentColor As Int, IsPauseSegment As Boolean) As String If IsPauseSegment Then Return "pausa" If SegmentColor = Colors.Red Then Return "straordinario" Return "lavoro" End Sub Private Sub GetCurrentWorkColor As Int If ProductiveElapsedMs < FirstWorkLimitMs Then Return WorkMorningColor If ProductiveElapsedMs < WorkLimitMs Then Return Colors.Green Return Colors.Red End Sub Private Sub GetNextWorkBoundary As Long If ProductiveElapsedMs < FirstWorkLimitMs Then Return FirstWorkLimitMs If ProductiveElapsedMs < WorkLimitMs Then Return WorkLimitMs Return MaxProductiveMs End Sub Private Sub UpdateElapsedLabel If lblElapsedTime.IsInitialized Then Dim labelPrefix As String = Localization.T("work") Dim dayStart As Long = GetDisplayedWorkDayStart If dayStart > 0 Then labelPrefix = FormatDayShort(dayStart) & " " & Localization.T("work") lblElapsedTime.Text = labelPrefix & ": " & FormatElapsedTime(GetDisplayedProductiveElapsed(DateTime.Now)) End If End Sub Private Sub UpdateMainControlState If BtnStart.IsInitialized = False Then Return BtnStart.Enabled = AutoModeEnabled = False If SessionActive Or (AutoModeEnabled And AutoState = "recording") Then BtnStart.Color = Colors.RGB(0, 150, 70) BtnStart.TextColor = Colors.White Else BtnStart.Color = Colors.RGB(190, 40, 40) BtnStart.TextColor = Colors.White End If If AutoModeEnabled Then BtnPause.Enabled = False Else If SessionActive Then BtnPause.Enabled = True End If If lblAutoStatus.IsInitialized Then If AutoModeEnabled Then Select AutoState Case "recording" lblAutoStatus.Text = Localization.T("auto_recording") Case "paused" lblAutoStatus.Text = Localization.T("auto_paused") Case Else lblAutoStatus.Text = Localization.T("auto_stopped") End Select Else lblAutoStatus.Text = Localization.T("manual_mode") End If End If End Sub Private Sub GetDisplayedWorkDayStart As Long If CurrentWorkDayStart > 0 Then Return CurrentWorkDayStart Return GetLatestWorkDayStart End Sub Private Sub FormatDayShort(ticks As Long) As String Return NumberFormat(DateTime.GetDayOfMonth(ticks), 2, 0) & "/" & NumberFormat(DateTime.GetMonth(ticks), 2, 0) End Sub Private Sub GetDisplayedProductiveElapsed(now As Long) As Long Dim total As Long = ProductiveElapsedMs If SessionActive And PauseActive And IncludePauseInTotal Then total = total + Max(0, now - CurrentSegmentStart) If SessionActive And PauseActive = False Then total = total + Max(0, now - CurrentSegmentStart) Return total End Sub Private Sub GetProductiveTotalForWorkDay(DayKey As Long) As Long Dim total As Long = 0 For Each row As Map In StatsRows If row.Get("dayKey") = DayKey Then Dim category As String = row.Get("category") If category = "lavoro" Or category = "straordinario" Then total = total + row.Get("duration") If IncludePauseInTotal And category = "pausa" Then total = total + row.Get("duration") End If Next Return total End Sub Private Sub LoadWorkDaySegmentsIntoClock(DayKey As Long) Dim productiveCursor As Long = 0 For Each row As Map In StatsRows If row.Get("dayKey") = DayKey Then Dim category As String = row.Get("category") Dim duration As Long = row.Get("duration") If category = "pausa" Then myClock.AddSegment(duration, Colors.Yellow, True) If IncludePauseInTotal Then productiveCursor = productiveCursor + duration Else AddDisplayProductiveSegment(duration, productiveCursor) productiveCursor = productiveCursor + duration End If End If Next End Sub Private Sub AddDisplayProductiveSegment(Duration As Long, StartProductiveMs As Long) Dim remaining As Long = Duration Dim cursor As Long = StartProductiveMs Do While remaining > 0 Dim segmentColor As Int Dim boundary As Long If cursor < FirstWorkLimitMs Then segmentColor = WorkMorningColor boundary = FirstWorkLimitMs Else If cursor < WorkLimitMs Then segmentColor = Colors.Green boundary = WorkLimitMs Else segmentColor = Colors.Red boundary = MaxProductiveMs End If Dim chunk As Long = remaining If cursor < boundary Then chunk = Min(chunk, boundary - cursor) myClock.AddSegment(chunk, segmentColor, False) remaining = remaining - chunk cursor = cursor + chunk Loop End Sub Private Sub GetWorkDayStart(now As Long) As Long Dim latestDayStart As Long = 0 Dim today As String = DateTime.Date(now) For Each row As Map In StatsRows Dim dayStart As Long = row.Get("dayStart") If dayStart > latestDayStart And DateTime.Date(dayStart) = today Then latestDayStart = dayStart End If Next If latestDayStart > 0 Then Return latestDayStart Return now End Sub Private Sub GetLatestWorkDayStart As Long Dim latestDayStart As Long = 0 For Each row As Map In StatsRows Dim dayStart As Long = row.Get("dayStart") If dayStart > latestDayStart Then latestDayStart = dayStart Next Return latestDayStart End Sub Private Sub GetNextMidnight(ticks As Long) As Long Return DateTime.DateParse(DateTime.Date(ticks)) + DateTime.TicksPerDay End Sub Private Sub LoadStatsRows StatsRows.Clear If File.Exists(File.DirInternal, StatsFileName) = False Then Return Dim lines As List = File.ReadList(File.DirInternal, StatsFileName) For Each line As String In lines If line.Trim.Length > 0 Then Dim parts() As String = Regex.Split("\|", line) If parts.Length >= 4 Then Dim row As Map row.Initialize Dim dayStart As Long = parts(0) Dim segmentStart As Long = parts(1) Dim duration As Long = parts(2) row.Put("dayStart", dayStart) row.Put("dayKey", dayStart) row.Put("segmentStart", segmentStart) row.Put("date", DateTime.Date(dayStart)) row.Put("duration", duration) row.Put("category", parts(3)) If parts.Length >= 5 Then row.Put("source", parts(4)) Else row.Put("source", "manual") End If StatsRows.Add(row) End If End If Next TrimStatsRows SaveStatsRows End Sub Private Sub SaveStatsRows Dim lines As List lines.Initialize For Each row As Map In StatsRows Dim source As String = "manual" If row.ContainsKey("source") Then source = row.Get("source") lines.Add(row.Get("dayStart") & "|" & row.Get("segmentStart") & "|" & row.Get("duration") & "|" & row.Get("category") & "|" & source) Next File.WriteList(File.DirInternal, StatsFileName, lines) End Sub Private Sub TrimStatsRows Dim retention As Long = MaxStoredDays retention = retention * DateTime.TicksPerDay Dim cutoff As Long = DateTime.Now - retention Dim kept As List kept.Initialize For Each row As Map In StatsRows Dim dayStart As Long = row.Get("dayStart") If dayStart >= cutoff Then kept.Add(row) Next StatsRows.Clear StatsRows.AddAll(kept) End Sub Private Sub SaveActiveState If SessionActive = False Then If File.Exists(File.DirInternal, StateFileName) Then File.Delete(File.DirInternal, StateFileName) LastActiveStateSaveAt = 0 Return End If Dim lines As List lines.Initialize lines.Add(CurrentWorkDayStart) lines.Add(CurrentSegmentStart) lines.Add(ProductiveElapsedMs) lines.Add(PauseActive) lines.Add(CurrentSegmentColor) File.WriteList(File.DirInternal, StateFileName, lines) LastActiveStateSaveAt = DateTime.Now End Sub Private Sub RestoreActiveState If SessionActive Then Return If File.Exists(File.DirInternal, StateFileName) = False Then Return Dim lines As List = File.ReadList(File.DirInternal, StateFileName) If lines.Size < 5 Then Return CurrentWorkDayStart = lines.Get(0) CurrentWorkDayKey = CurrentWorkDayStart CurrentSegmentStart = lines.Get(1) ProductiveElapsedMs = GetProductiveTotalForWorkDay(CurrentWorkDayKey) PauseActive = lines.Get(3) CurrentSegmentColor = lines.Get(4) Dim now As Long = DateTime.Now If now >= GetNextMidnight(CurrentSegmentStart) Then SessionActive = True EndSession(GetNextMidnight(CurrentSegmentStart), True) If File.Exists(File.DirInternal, StateFileName) Then File.Delete(File.DirInternal, StateFileName) Return End If If ProductiveElapsedMs >= MaxProductiveMs Then If File.Exists(File.DirInternal, StateFileName) Then File.Delete(File.DirInternal, StateFileName) Return End If SessionActive = True BtnStart.Text = Localization.T("end") If PauseActive Then BtnPause.Text = Localization.T("end_pause") BtnPause.Enabled = True Else BtnPause.Text = Localization.T("pause") BtnPause.Enabled = True CurrentSegmentColor = GetCurrentWorkColor End If UpdateMainControlState myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.SetActiveSegment(Max(0, now - CurrentSegmentStart), CurrentSegmentColor, PauseActive) UpdateElapsedLabel End Sub Private Sub RestoreLastClosedDayState If SessionActive Then Return Dim latestDayStart As Long = GetLatestWorkDayStart If latestDayStart = 0 Then Return If CurrentWorkDayStart = latestDayStart And ProductiveElapsedMs > 0 Then Return CurrentWorkDayStart = latestDayStart CurrentWorkDayKey = latestDayStart ProductiveElapsedMs = GetProductiveTotalForWorkDay(CurrentWorkDayKey) CurrentSegmentStart = 0 PauseActive = False CurrentSegmentColor = GetCurrentWorkColor myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.ClearActiveSegment UpdateElapsedLabel End Sub Private Sub ShowStatsPage BuildStatsTable SetMainControlsVisible(False) pnlStats.Visible = True pnlConfig.Visible = False pnlHelp.Visible = False pnlStats.BringToFront End Sub Private Sub SetMainControlsVisible(Visible As Boolean) For i = 0 To Activity.NumberOfViews - 1 Dim v As View = Activity.GetView(i) v.Visible = Visible Next HideLegacyButtons pnlStats.Visible = False pnlConfig.Visible = False pnlHelp.Visible = False End Sub Private Sub HideLegacyButtons If BtnReset.IsInitialized Then BtnReset.Enabled = False BtnReset.Visible = False BtnReset.SetLayout(-200dip, -200dip, 1dip, 1dip) End If If BtnSync.IsInitialized Then BtnSync.Enabled = False BtnSync.Visible = False BtnSync.SetLayout(-200dip, -200dip, 1dip, 1dip) End If For i = Activity.NumberOfViews - 1 To 0 Step -1 Dim v As View = Activity.GetView(i) If v Is Button Then Dim legacy As Button = v If legacy.Text = "Sync" Or legacy.Text = "Reset" Then legacy.RemoveView End If End If Next End Sub Private Sub ShowConfigPage SetMainControlsVisible(False) pnlStats.Visible = False pnlConfig.Visible = True pnlHelp.Visible = False pnlConfig.BringToFront End Sub Private Sub ShowHelpPage SetMainControlsVisible(False) pnlStats.Visible = False pnlConfig.Visible = False pnlHelp.Visible = True pnlHelp.BringToFront End Sub Private Sub ShowMainPage SetMainControlsVisible(True) pnlStats.Visible = False pnlConfig.Visible = False pnlHelp.Visible = False End Sub Private Sub BuildStatsTable svStats.Panel.RemoveAllViews Dim y As Int = 0 AddStatsRow(y, Localization.T("date"), Localization.T("work"), Localization.T("pause_col"), Localization.T("overtime"), True) y = y + 34dip Dim totalWork As Long = 0 Dim totalPause As Long = 0 Dim totalOvertime As Long = 0 Dim dailyRows As Map dailyRows.Initialize For Each row As Map In StatsRows AddStatsDuration(dailyRows, row.Get("date"), row.Get("category"), row.Get("duration")) Next If SessionActive Then Dim activeDuration As Long = Max(0, DateTime.Now - CurrentSegmentStart) If activeDuration > 0 Then AddStatsDuration(dailyRows, DateTime.Date(CurrentWorkDayStart), GetSegmentCategory(CurrentSegmentColor, PauseActive), activeDuration) End If End If For Each dateText As String In dailyRows.Keys Dim dailyTotals As Map = dailyRows.Get(dateText) totalWork = totalWork + dailyTotals.Get("lavoro") totalPause = totalPause + dailyTotals.Get("pausa") totalOvertime = totalOvertime + dailyTotals.Get("straordinario") Next For Each dateText As String In dailyRows.Keys Dim dailyTotals As Map = dailyRows.Get(dateText) AddStatsRow(y, dateText, FormatElapsedTime(dailyTotals.Get("lavoro")), FormatElapsedTime(dailyTotals.Get("pausa")), FormatElapsedTime(dailyTotals.Get("straordinario")), False) y = y + 30dip Next y = y + 8dip AddStatsRow(y, Localization.T("totals"), FormatElapsedTime(totalWork), FormatElapsedTime(totalPause), FormatElapsedTime(totalOvertime), True) y = y + 40dip svStats.Panel.Height = Max(y, svStats.Height + 1dip) End Sub Private Sub AddStatsDuration(dailyRows As Map, DateText As String, Category As String, Duration As Long) Dim dailyTotals As Map If dailyRows.ContainsKey(DateText) Then dailyTotals = dailyRows.Get(DateText) Else dailyTotals.Initialize dailyTotals.Put("lavoro", 0) dailyTotals.Put("pausa", 0) dailyTotals.Put("straordinario", 0) dailyRows.Put(DateText, dailyTotals) End If dailyTotals.Put(Category, dailyTotals.Get(Category) + Duration) End Sub Private Sub SetAutomaticConfig Dim startMinutes As Int = ParseTimeToMinutes(txtCfgStart.Text) Dim pauseStartMinutes As Int = ParseTimeToMinutes(txtCfgPauseStart.Text) Dim pauseEndMinutes As Int = ParseTimeToMinutes(txtCfgPauseEnd.Text) Dim endMinutes As Int = ParseTimeToMinutes(txtCfgEnd.Text) If startMinutes < 0 Or pauseStartMinutes < 0 Or pauseEndMinutes < 0 Or endMinutes < 0 Then ToastMessageShow(Localization.T("use_hhmm"), False) Return End If If startMinutes >= pauseStartMinutes Or pauseStartMinutes >= pauseEndMinutes Or pauseEndMinutes >= endMinutes Then ToastMessageShow(Localization.T("times_order"), False) Return End If Dim workMinutes As Int = (pauseStartMinutes - startMinutes) + (endMinutes - pauseEndMinutes) Dim countedMinutes As Int = workMinutes If IncludePauseInTotal Then countedMinutes = endMinutes - startMinutes If countedMinutes > 480 Then ToastMessageShow(Localization.T("work_exceed"), False) Return End If AutoStartMinutes = startMinutes AutoPauseStartMinutes = pauseStartMinutes AutoPauseEndMinutes = pauseEndMinutes AutoEndMinutes = endMinutes AutoModeEnabled = True SaveAutoConfig If SessionActive Then EndSession(DateTime.Now, False) If File.Exists(File.DirInternal, StateFileName) Then File.Delete(File.DirInternal, StateFileName) UpdateAutomaticState(DateTime.Now) ToastMessageShow(Localization.T("auto_enabled"), False) End Sub Private Sub ResetAutomaticConfig AutoModeEnabled = False AutoState = "stopped" If File.Exists(File.DirInternal, AutoConfigFileName) Then File.Delete(File.DirInternal, AutoConfigFileName) ResetSessionState(False) RestoreLastClosedDayState UpdateMainControlState ToastMessageShow(Localization.T("auto_disabled"), False) End Sub Private Sub SaveAutoConfig Dim lines As List lines.Initialize lines.Add(AutoStartMinutes) lines.Add(AutoPauseStartMinutes) lines.Add(AutoPauseEndMinutes) lines.Add(AutoEndMinutes) File.WriteList(File.DirInternal, AutoConfigFileName, lines) End Sub Private Sub SaveSettings Dim lines As List lines.Initialize lines.Add(IncludePauseInTotal) File.WriteList(File.DirInternal, SettingsFileName, lines) End Sub Private Sub LoadAutoConfig AutoModeEnabled = False AutoState = "stopped" If File.Exists(File.DirInternal, AutoConfigFileName) = False Then Return Dim lines As List = File.ReadList(File.DirInternal, AutoConfigFileName) If lines.Size < 4 Then Return AutoStartMinutes = lines.Get(0) AutoPauseStartMinutes = lines.Get(1) AutoPauseEndMinutes = lines.Get(2) AutoEndMinutes = lines.Get(3) AutoModeEnabled = True txtCfgStart.Text = MinutesToTime(AutoStartMinutes) txtCfgPauseStart.Text = MinutesToTime(AutoPauseStartMinutes) txtCfgPauseEnd.Text = MinutesToTime(AutoPauseEndMinutes) txtCfgEnd.Text = MinutesToTime(AutoEndMinutes) End Sub Private Sub LoadSettings IncludePauseInTotal = True If File.Exists(File.DirInternal, SettingsFileName) = False Then Return Dim lines As List = File.ReadList(File.DirInternal, SettingsFileName) If lines.Size = 0 Then Return IncludePauseInTotal = ParseBooleanValue(lines.Get(0), True) End Sub Private Sub ParseTimeToMinutes(Value As String) As Int Dim parts() As String = Regex.Split(":", Value.Trim) If parts.Length <> 2 Then Return -1 If IsNumber(parts(0)) = False Or IsNumber(parts(1)) = False Then Return -1 Dim h As Int = parts(0) Dim m As Int = parts(1) If h < 0 Or h > 23 Or m < 0 Or m > 59 Then Return -1 Return h * 60 + m End Sub Private Sub MinutesToTime(Minutes As Int) As String Return NumberFormat(Minutes / 60, 2, 0) & ":" & NumberFormat(Minutes Mod 60, 2, 0) End Sub Private Sub UpdateAutomaticState(now As Long) Dim dayStart As Long = DateTime.DateParse(DateTime.Date(now)) Dim minuteOfDay As Int = DateTime.GetHour(now) * 60 + DateTime.GetMinute(now) CurrentWorkDayStart = dayStart CurrentWorkDayKey = dayStart RegenerateAutoSegmentsForDay(dayStart, minuteOfDay) ProductiveElapsedMs = GetProductiveTotalForWorkDay(CurrentWorkDayKey) SessionActive = minuteOfDay >= AutoStartMinutes And minuteOfDay < AutoEndMinutes And ProductiveElapsedMs < WorkLimitMs PauseActive = minuteOfDay >= AutoPauseStartMinutes And minuteOfDay < AutoPauseEndMinutes If SessionActive And PauseActive = False Then AutoState = "recording" Else If SessionActive And PauseActive Then AutoState = "paused" Else AutoState = "stopped" End If BtnStart.Text = IIf(SessionActive, Localization.T("end"), Localization.T("start")) BtnPause.Text = IIf(PauseActive, Localization.T("end_pause"), Localization.T("pause")) CurrentSegmentStart = now CurrentSegmentColor = GetCurrentWorkColor myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.ClearActiveSegment UpdateElapsedLabel UpdateMainControlState End Sub Private Sub RegenerateAutoSegmentsForDay(DayStart As Long, MinuteOfDay As Int) RemoveAutoRowsForDay(DayStart) Dim firstWorkEnd As Int = Min(MinuteOfDay, AutoPauseStartMinutes) If firstWorkEnd > AutoStartMinutes Then AddAutoSegmentNoSave(DayStart, AutoStartMinutes, firstWorkEnd, "lavoro") End If Dim pauseEnd As Int = Min(MinuteOfDay, AutoPauseEndMinutes) If pauseEnd > AutoPauseStartMinutes Then AddAutoSegmentNoSave(DayStart, AutoPauseStartMinutes, pauseEnd, "pausa") End If Dim secondWorkEnd As Int = Min(MinuteOfDay, AutoEndMinutes) If secondWorkEnd > AutoPauseEndMinutes Then AddAutoSegmentNoSave(DayStart, AutoPauseEndMinutes, secondWorkEnd, "lavoro") End If TrimStatsRows SaveStatsRows End Sub Private Sub AddAutoSegmentNoSave(DayStart As Long, FromMinutes As Int, ToMinutes As Int, Category As String) Dim duration As Long = (ToMinutes - FromMinutes) * DateTime.TicksPerMinute If duration <= 0 Then Return Dim row As Map row.Initialize row.Put("dayStart", DayStart) row.Put("dayKey", DayStart) row.Put("segmentStart", DayStart + FromMinutes * DateTime.TicksPerMinute) row.Put("date", DateTime.Date(DayStart)) row.Put("duration", duration) row.Put("category", Category) row.Put("source", "auto") StatsRows.Add(row) End Sub Private Sub RemoveAutoRowsForDay(DayStart As Long) Dim kept As List kept.Initialize For Each row As Map In StatsRows Dim source As String = "manual" If row.ContainsKey("source") Then source = row.Get("source") If row.Get("dayStart") = DayStart And source = "auto" Then ' skip regenerated automatic segment Else kept.Add(row) End If Next StatsRows.Clear StatsRows.AddAll(kept) End Sub Private Sub ClearDisplayedDay Dim dayStart As Long = GetDisplayedWorkDayStart If dayStart = 0 Then ToastMessageShow(Localization.T("no_day_to_clear"), False) Return End If Dim result As Int = Msgbox2(Localization.T("clear_today_confirm") & " " & FormatDayShort(dayStart) & "?", Localization.T("clear_today_title"), Localization.T("clear_button"), Localization.T("cancel"), "", Null) If result <> DialogResponse.POSITIVE Then Return Dim kept As List kept.Initialize For Each row As Map In StatsRows If row.Get("dayStart") <> dayStart Then kept.Add(row) Next StatsRows.Clear StatsRows.AddAll(kept) SaveStatsRows If File.Exists(File.DirInternal, StateFileName) Then File.Delete(File.DirInternal, StateFileName) ResetSessionState(True) If AutoModeEnabled Then UpdateAutomaticState(DateTime.Now) Else RestoreLastClosedDayState End If ToastMessageShow(Localization.T("today_cleared"), False) End Sub Private Sub ApplyPauseCountingMode If chkIncludePauses.IsInitialized And chkIncludePauses.Checked <> IncludePauseInTotal Then chkIncludePauses.Checked = IncludePauseInTotal Dim now As Long = DateTime.Now If SessionActive Then ProductiveElapsedMs = GetProductiveTotalForWorkDay(CurrentWorkDayKey) If PauseActive Then ProcessActivePauseSegment(now) If SessionActive = False Then UpdateElapsedLabel Return End If CurrentSegmentColor = Colors.Yellow myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.SetActiveSegment(Max(0, now - CurrentSegmentStart), Colors.Yellow, True) Else ProcessActiveWorkSegment(now) If SessionActive = False Then UpdateElapsedLabel Return End If CurrentSegmentColor = GetCurrentWorkColor myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.SetActiveSegment(Max(0, now - CurrentSegmentStart), CurrentSegmentColor, False) End If SaveActiveState Else If CurrentWorkDayKey > 0 Then ProductiveElapsedMs = GetProductiveTotalForWorkDay(CurrentWorkDayKey) CurrentSegmentColor = GetCurrentWorkColor myClock.ClearSegments LoadWorkDaySegmentsIntoClock(CurrentWorkDayKey) myClock.ClearActiveSegment Else RestoreLastClosedDayState End If End If UpdateElapsedLabel UpdateMainControlState End Sub Private Sub ParseBooleanValue(Value As Object, DefaultValue As Boolean) As Boolean If Value = Null Then Return DefaultValue Dim s As String = Value s = s.Trim.ToLowerCase If s = "true" Then Return True If s = "false" Then Return False Return DefaultValue End Sub Private Sub AddStatsRow(y As Int, DateText As String, WorkText As String, PauseText As String, OvertimeText As String, Header As Boolean) Dim rowHeight As Int = 30dip Dim col1 As Int = 104dip Dim col As Int = (Activity.Width - col1) / 3 AddCell(DateText, 0, y, col1, rowHeight, Header) AddCell(WorkText, col1, y, col, rowHeight, Header) AddCell(PauseText, col1 + col, y, col, rowHeight, Header) AddCell(OvertimeText, col1 + 2 * col, y, col, rowHeight, Header) End Sub Private Sub AddCell(Text As String, x As Int, y As Int, w As Int, h As Int, Header As Boolean) Dim lbl As Label lbl.Initialize("") lbl.Text = Text lbl.TextSize = 12 lbl.TextColor = Colors.Black lbl.Gravity = Gravity.CENTER If Header Then lbl.Color = Colors.RGB(230, 230, 230) Else lbl.Color = Colors.White End If svStats.Panel.AddView(lbl, x, y, w, h) End Sub Private Sub BuildHelpText As String Return $"Overtime Guard By AI Team Studio aiteamstudio.it AI agents and software systems Purpose Overtime Guard is a minimal workday timer. It helps you track work time, breaks, and overtime without projects, tasks, or extra screens. Main screen - Start begins manual recording for the current workday. - End stops manual recording. - Pause starts a break. - End pause closes the break and resumes work. - Bg sends the app to the Android home screen. - Stats opens the daily totals table. - Config opens automatic schedule settings. Clock colors - Light blue: first 4 hours of productive work. - Green: productive work from hour 5 to hour 8. - Yellow: break time. - Red: overtime after 8 productive hours. How timing works - Breaks always stay visible in yellow. - In Config you can choose whether break time is included in the total 8-hour count. - Overtime starts only after 8 productive work hours. - Red overtime can continue for up to 1 extra hour. - At 9 total productive hours, recording stops automatically. - If midnight is reached, the current day is closed and the next day starts from zero when you start again. Daily memory - The app saves the current day locally. - If you close or background the app, the state is restored when you open it again. - During the day, multiple work and break segments are added to the same daily record. Statistics - The stats page groups totals by date. - Each row shows Work, Pause, and Overtime for one day. - The totals row sums all visible stored days. - Local history is currently kept for about two months. Automatic mode - In Config you can set work start, pause start, pause end, and work end. - The Include breaks in total option also affects automatic mode. - Set config enables automatic mode. - In automatic mode, Start and End on the main screen are disabled. - Reset config disables automatic mode and returns control to manual mode. Clear today - Clear today removes only the currently displayed workday from local history. - Use it only if you want to restart the day from zero. Notes - The app uses the phone local time and time zone. - The help page is informational only. It does not change your data. "$ End Sub