1397 lines
44 KiB
Plaintext
1397 lines
44 KiB
Plaintext
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~<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34"/>~\n~<supports-screens android:largeScreens="true" ~\n~ android:normalScreens="true" ~\n~ android:smallScreens="true" ~\n~ android:anyDensity="true"/>)~\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 | aiteam.ai"
|
|
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
|
|
aiteam.ai
|
|
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
|