]> repos.lx-s.de Git - winmute.git/commitdiff
Added quiet hours execution
authorlx-s <lx-s@users.noreply.github.com>
Sat, 7 Mar 2026 15:19:03 +0000 (16:19 +0100)
committerlx-s <lx-s@users.noreply.github.com>
Sat, 7 Mar 2026 15:19:03 +0000 (16:19 +0100)
Translations/lang-de.json
Translations/lang-en.json
WinMute/QuietHoursTimer.cpp
WinMute/QuietHoursTimer.h
WinMute/Settings_QuietHoursDlg.cpp
WinMute/VistaAudio.cpp

index 100fd5d4597a7c91028fe8e63bb24f8e517716d7..051462752d9c9bb89c2bbcba3ed75c747e123169 100644 (file)
   "settings.quiet-hours.error.error-while-saving.text": "Während dem Speichern der Ruhezeiteinstellungen trat ein Fehler auf",
   "settings.quiet-hours.add-edit.add-title": "Ruhezeit hinzufügen",
   "settings.quiet-hours.add-edit.edit-title": "Ruhezeit bearbeiten",
+  "settings.quiet-hours.error.overlapping-time-range.title": "Überlappender Zeitraum",
+  "settings.quiet-hours.error.overlapping-time-range.text": "Dieser Zeitraum überschneidet sich mit einem vorhandenen Eintrag.",
   "settings.wifi.intro": "Das WLAN basiert Stummschalten ermöglicht es die PC-Lautstärke abhängig von dem Namen des verbundenen WLAN Netzwerks zu steuern.",
   "settings.wifi.enable": "Aktiviere WLAN-basiertes Stummschalten",
   "settings.wifi.mute-when-not-in-list": "Stummschalten, wenn das WLAN nicht in der Liste ist",
index 9ecb8e325f4e224c94afdfcd06502a0eaf97c418..f62e52d10b21419fac547009ca824aa5ac894335 100644 (file)
   "settings.quiet-hours.error.error-while-saving.text": "Something went wrong while saving the settings",
   "settings.quiet-hours.add-edit.add-title": "Add quiet hours",
   "settings.quiet-hours.add-edit.edit-title": "Edit quiet hours",
+  "settings.quiet-hours.error.overlapping-time-range.title": "Overlapping time range",
+  "settings.quiet-hours.error.overlapping-time-range.text": "This time range overlaps with an existing entry.",
   "settings.wifi.intro": "\"WLAN Mute\" mutes the computer based on the name of the connected wireless network.",
   "settings.wifi.enable": "Enable WLAN based muting",
   "settings.wifi.mute-when-in-list": "Mute if connected WLAN is in list",
index 2c019727f0636655332af308f2b8ad7d1b1ce680..56b997c86071bc4e5b18291e503206610c50aeb0 100644 (file)
@@ -35,80 +35,48 @@ POSSIBILITY OF SUCH DAMAGE.
 
 extern HINSTANCE hglobInstance;
 
-static constexpr int QUIETHOURS_TIMER_START_ID = 271020;
-static constexpr int QUIETHOURS_TIMER_END_ID = 271021;
+static constexpr UINT_PTR QUIETHOURS_TIMER_START_ID = 271020;
+static constexpr UINT_PTR QUIETHOURS_TIMER_END_ID   = 271021;
+static constexpr DWORD    SECONDS_PER_DAY           = 86400;
 
-static std::int64_t ConvertSystemTimeTo100NS(const LPSYSTEMTIME sysTime) noexcept
+// Returns seconds since midnight for the current local time
+static DWORD GetNowSeconds() noexcept
 {
-   FILETIME ft;
-   SystemTimeToFileTime(sysTime, &ft);
-   ULARGE_INTEGER ulInt;
-   ulInt.HighPart = ft.dwHighDateTime;
-   ulInt.LowPart = ft.dwLowDateTime;
-   return static_cast<std::int64_t>(ulInt.QuadPart);
-}
-
-static bool IsQuietHours(
-   const LPSYSTEMTIME now,
-   const LPSYSTEMTIME qhStart,
-   const LPSYSTEMTIME qhEnd)
-{
-   const std::int64_t iStart = ConvertSystemTimeTo100NS(qhStart);
-   std::int64_t iEnd = ConvertSystemTimeTo100NS(qhEnd);
-   const std::int64_t iNow = ConvertSystemTimeTo100NS(now);
-
-   if (iEnd < iStart) {
-      // Add one day
-      iEnd += 864000000000ll;
-   }
-
-   if (iEnd - iNow > 0 && iStart - iNow < 0) {
-      return true;
-   }
-
-   return false;
+   SYSTEMTIME now;
+   GetLocalTime(&now);
+   return static_cast<DWORD>(now.wHour) * 3600
+        + static_cast<DWORD>(now.wMinute) * 60
+        + static_cast<DWORD>(now.wSecond);
 }
 
-/**
- * Gets the number of milliseconds that t2 would have to add, to reach t1.
- * Takes day wrap around into consideration.
- */
-static int GetDiffMillseconds(const LPSYSTEMTIME t1, const LPSYSTEMTIME t2)
+// Returns true if t falls within [wStart, wEnd) on the circular 24-hour clock
+static bool TimeInWindow(DWORD t, DWORD wStart, DWORD wEnd) noexcept
 {
-   __int64 it1 = ConvertSystemTimeTo100NS(t1);
-   __int64 it2 = ConvertSystemTimeTo100NS(t2);
-
-   // 100NS to Milliseconds;
-   it1 /= 10000;
-   it2 /= 10000;
-   if (it1 < it2) { // add one day wrap around
-      it1 += 24 * 60 * 60 * 1000;
+   if (wStart < wEnd) {
+      return t >= wStart && t < wEnd;
+   } else { // wraps midnight
+      return t >= wStart || t < wEnd;
    }
-   __int64 res = it1 - it2;
-
-   return static_cast<int>(res);
 }
 
-static VOID CALLBACK QuietHoursTimerProc(HWND hWnd, UINT msg, UINT_PTR id, DWORD msSinceSysStart) noexcept
+static VOID CALLBACK QuietHoursTimerProc(
+   HWND hWnd, UINT /*msg*/, UINT_PTR id, DWORD /*msSinceSysStart*/) noexcept
 {
-   UNREFERENCED_PARAMETER(msg);
-   UNREFERENCED_PARAMETER(msSinceSysStart);
+   KillTimer(hWnd, id);
    if (id == QUIETHOURS_TIMER_START_ID) {
-      KillTimer(hWnd, id);
       SendMessageW(hWnd, WM_WINMUTE_QUIETHOURS_START, 0, 0);
-   }
-   else if (id == QUIETHOURS_TIMER_END_ID) {
-      KillTimer(hWnd, id);
+   } else if (id == QUIETHOURS_TIMER_END_ID) {
       SendMessageW(hWnd, WM_WINMUTE_QUIETHOURS_END, 0, 0);
    }
 }
 
+// ---------------------------------------------------------------------------
+
 QuietHoursTimer::QuietHoursTimer() :
-   initialized_{ false },
-   enabled_{ false },
-   hParent_{ nullptr },
-   qhStart_{0},
-   qhEnd_{ 0 }
+   hParent_(nullptr),
+   initialized_(false),
+   enabled_(false),
+   activeWindowIdx_(-1)
 {
 }
 
@@ -120,149 +88,163 @@ QuietHoursTimer::~QuietHoursTimer()
    }
 }
 
-bool QuietHoursTimer::LoadFromSettings(const WMSettings& settings)
+// Returns the index of the window that currently contains the local time,
+// or -1 if none is active.
+int QuietHoursTimer::FindActiveWindowIdx() const
 {
-   KillTimer(hParent_, QUIETHOURS_TIMER_START_ID);
-   KillTimer(hParent_, QUIETHOURS_TIMER_END_ID);
-
-   if (!settings.QueryValue(SettingsKey::QUIETHOURS_ENABLE)) {
-      enabled_ = false;
-      return true;
+   const DWORD now = GetNowSeconds();
+   for (int i = 0; i < static_cast<int>(windows_.size()); ++i) {
+      if (TimeInWindow(now, windows_[i].first, windows_[i].second)) {
+         return i;
+      }
    }
+   return -1;
+}
 
-   qhStart_ = settings.QueryValue(SettingsKey::QUIETHOURS_START_V0_DEPRECATED);
-   qhEnd_ = settings.QueryValue(SettingsKey::QUIETHOURS_END_V0_DEPRECATED);
-
-   SYSTEMTIME now;
-   SYSTEMTIME start;
-   SYSTEMTIME end;
-   GetLocalTime(&now);
-   GetLocalTime(&start);
-   GetLocalTime(&end);
-
-   start.wSecond = static_cast<WORD>(qhStart_ % 60); 
-   start.wMinute = static_cast<WORD>(((qhStart_ - start.wSecond) / 60) % 60);
-   start.wHour = static_cast<WORD>((qhStart_ - start.wMinute - start.wSecond) / 3600);
-
-   end.wSecond = static_cast<WORD>(qhEnd_ % 60);
-   end.wMinute = static_cast<WORD>(((qhEnd_ - end.wSecond) / 60) % 60);
-   end.wHour = static_cast<WORD>((qhEnd_ - end.wMinute - end.wSecond) / 3600);
-
-   if (IsQuietHours(&now, &start, &end)) {
-      WMLog::GetInstance().LogInfo(L"Mute: On | Quiet hours have already started");
-      SendMessage(hParent_, WM_WINMUTE_QUIETHOURS_START, 0, 0);
-   } else {
-      int timerQhStart = GetDiffMillseconds(&start, &now);
-      if (SetTimer(
-            hParent_,
-            QUIETHOURS_TIMER_START_ID,
-            timerQhStart,
-            QuietHoursTimerProc) == 0) {
-         TaskDialog(
-            hParent_,
-            hglobInstance,
-            PROGRAM_NAME,
-            WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.title").c_str(),
-            WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.text").c_str(),
-            TDCBF_OK_BUTTON,
-            TD_ERROR_ICON,
-            nullptr);
-         return false;
+// Returns the index of the window whose start time is soonest in the future
+// (wrapping around midnight if necessary).
+int QuietHoursTimer::FindNextWindowIdx() const
+{
+   if (windows_.empty()) return -1;
+
+   const DWORD now = GetNowSeconds();
+   int   bestIdx  = -1;
+   DWORD bestDiff = MAXDWORD;
+
+   for (int i = 0; i < static_cast<int>(windows_.size()); ++i) {
+      const DWORD s = windows_[i].first;
+      const DWORD diff = (s >= now) ? (s - now) : (SECONDS_PER_DAY - now + s);
+      if (diff < bestDiff) {
+         bestDiff = diff;
+         bestIdx  = i;
       }
    }
-   return true;
+   return bestIdx;
 }
 
 bool QuietHoursTimer::SetStart()
 {
-   SYSTEMTIME now;
-   SYSTEMTIME start;
+   KillTimer(hParent_, QUIETHOURS_TIMER_START_ID);
 
-   GetLocalTime(&now);
-   GetLocalTime(&start);
-
-   start.wSecond = static_cast<WORD>(qhStart_ % 60);
-   start.wMinute = static_cast<WORD>(((qhStart_ - start.wSecond) / 60) % 60);
-   start.wHour = static_cast<WORD>((qhStart_ - start.wMinute - start.wSecond) / 3600);
-
-   const int timerQhStart = GetDiffMillseconds(&start, &now);
-   if (SetTimer(
-         hParent_,
-         QUIETHOURS_TIMER_START_ID,
-         timerQhStart,
-         QuietHoursTimerProc) == 0) {
+   if (!enabled_ || windows_.empty()) return true;
+
+   const int idx = FindNextWindowIdx();
+   if (idx == -1) return true;
+
+   activeWindowIdx_ = idx;
+
+   const DWORD now     = GetNowSeconds();
+   const DWORD startSec = windows_[idx].first;
+   const DWORD diffSec  = (startSec >= now)
+      ? (startSec - now)
+      : (SECONDS_PER_DAY - now + startSec);
+
+   // diffSec of 0 shouldn't happen here (caller checked FindActiveWindowIdx first),
+   // but guard against it so the timer fires without an infinite-delay edge case.
+   const UINT msDelay = (diffSec > 0) ? (diffSec * 1000u) : 1u;
+
+   if (SetTimer(hParent_, QUIETHOURS_TIMER_START_ID, msDelay, QuietHoursTimerProc) == 0) {
       TaskDialog(
-         hParent_,
-         hglobInstance,
-         PROGRAM_NAME,
+         hParent_, hglobInstance, PROGRAM_NAME,
          WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.title").c_str(),
          WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.text").c_str(),
-         TDCBF_OK_BUTTON,
-         TD_ERROR_ICON,
-         nullptr);
+         TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr);
       return false;
    }
+
+   WMLog::GetInstance().LogInfo(
+      L"QuietHours: Next start scheduled in %u s (window %d, %02u:%02u-%02u:%02u)",
+      diffSec, idx,
+      windows_[idx].first / 3600,  (windows_[idx].first % 3600) / 60,
+      windows_[idx].second / 3600, (windows_[idx].second % 3600) / 60);
+
    return true;
 }
 
 bool QuietHoursTimer::SetEnd()
 {
-   SYSTEMTIME now;
-   SYSTEMTIME end;
+   KillTimer(hParent_, QUIETHOURS_TIMER_END_ID);
 
-   GetLocalTime(&now);
-   GetLocalTime(&end);
-
-   end.wSecond = static_cast<WORD>(qhEnd_ % 60);
-   end.wMinute = static_cast<WORD>(((qhEnd_ - end.wSecond) / 60) % 60);
-   end.wHour = static_cast<WORD>((qhEnd_ - end.wMinute - end.wSecond) / 3600);
-
-   int timerQhEnd = GetDiffMillseconds(&end, &now);
-   if (timerQhEnd <= 0) {
-      SendMessage(hParent_, WM_WINMUTE_QUIETHOURS_END, 0, 0);
-   } else if (SetTimer(
-         hParent_,
-         QUIETHOURS_TIMER_END_ID,
-         timerQhEnd,
-         QuietHoursTimerProc) == 0) {
+   if (!enabled_ || windows_.empty() || activeWindowIdx_ < 0) return true;
+
+   const DWORD now    = GetNowSeconds();
+   const DWORD endSec = windows_[activeWindowIdx_].second;
+   const DWORD diffSec = (endSec > now)
+      ? (endSec - now)
+      : (SECONDS_PER_DAY - now + endSec);
+
+   if (diffSec == 0) {
+      // End is right now — fire the message immediately instead of a 0-ms timer
+      SendMessageW(hParent_, WM_WINMUTE_QUIETHOURS_END, 0, 0);
+      return true;
+   }
+
+   if (SetTimer(hParent_, QUIETHOURS_TIMER_END_ID, diffSec * 1000u, QuietHoursTimerProc) == 0) {
       TaskDialog(
-         hParent_,
-         hglobInstance,
-         PROGRAM_NAME,
-         WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.title").c_str(),
-         WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.text").c_str(),
-         TDCBF_OK_BUTTON,
-         TD_ERROR_ICON,
-         nullptr);
+         hParent_, hglobInstance, PROGRAM_NAME,
+         WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-stop.title").c_str(),
+         WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-stop.text").c_str(),
+         TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr);
       return false;
    }
+
+   WMLog::GetInstance().LogInfo(
+      L"QuietHours: End scheduled in %u s (window %d)", diffSec, activeWindowIdx_);
+
    return true;
 }
 
-bool QuietHoursTimer::Init(HWND hParent, const WMSettings& settings)
+bool QuietHoursTimer::IsQuietTime() const
 {
-   if (initialized_) {
-      return true;
-   }
-   hParent_ = hParent;
-   initialized_ = true;
-   return LoadFromSettings(settings);
+   if (!enabled_ || windows_.empty()) return false;
+   return FindActiveWindowIdx() != -1;
 }
 
-bool QuietHoursTimer::IsQuietTime()
+bool QuietHoursTimer::LoadFromSettings(WMSettings& settings)
 {
-   SYSTEMTIME now;
-   SYSTEMTIME start;
-   SYSTEMTIME end;
-   GetLocalTime(&now);
-   GetLocalTime(&start);
-   GetLocalTime(&end);
-   return IsQuietHours(&now, &start, &end);
+   KillTimer(hParent_, QUIETHOURS_TIMER_START_ID);
+   KillTimer(hParent_, QUIETHOURS_TIMER_END_ID);
+   windows_.clear();
+   activeWindowIdx_ = -1;
+
+   enabled_ = settings.QueryValue(SettingsKey::QUIETHOURS_ENABLE) != 0;
+   if (!enabled_) return true;
+
+   // Load all defined time windows and sort them by start time
+   auto loaded = settings.GetQuietHoursTimes();
+   for (const auto& t : loaded) {
+      if (t.first != t.second) { // skip degenerate entries
+         windows_.push_back(t);
+      }
+   }
+   if (windows_.empty()) return true;
+
+   std::sort(windows_.begin(), windows_.end(),
+      [](const auto& a, const auto& b) { return a.first < b.first; });
+
+   // Check if we are already inside a quiet-hours window at startup
+   const int activeIdx = FindActiveWindowIdx();
+   if (activeIdx != -1) {
+      activeWindowIdx_ = activeIdx;
+      WMLog::GetInstance().LogInfo(L"QuietHours: Currently inside window %d — muting now", activeIdx);
+      // Sending START will cause WinMute to mute and call SetEnd()
+      SendMessageW(hParent_, WM_WINMUTE_QUIETHOURS_START, 0, 0);
+   } else {
+      SetStart();
+   }
+
+   return true;
 }
 
-bool QuietHoursTimer::Reset(const WMSettings& settings)
+bool QuietHoursTimer::Init(HWND hParent, WMSettings& settings)
 {
+   if (initialized_) return true;
+   hParent_     = hParent;
+   initialized_ = true;
    return LoadFromSettings(settings);
 }
 
-
+bool QuietHoursTimer::Reset(WMSettings& settings)
+{
+   return LoadFromSettings(settings);
+}
index 86222080f96015bc2d5e5f35fae4570deeca2fa8..928b307a0080f5c172ecffa068f09119bfe688ed 100644 (file)
@@ -45,21 +45,23 @@ public:
    QuietHoursTimer(const QuietHoursTimer&) = delete;
    QuietHoursTimer& operator=(const QuietHoursTimer&) = delete;
 
-   bool Init(HWND hParent, const WMSettings& settings);
+   bool Init(HWND hParent, WMSettings& settings);
 
-   bool IsQuietTime();
+   bool IsQuietTime() const;
 
    bool SetStart();
    bool SetEnd();
 
-   bool Reset(const WMSettings& settings);
+   bool Reset(WMSettings& settings);
 
 private:
    HWND hParent_;
    bool initialized_;
    bool enabled_;
-   time_t qhStart_;
-   time_t qhEnd_;
+   std::vector<std::pair<DWORD, DWORD>> windows_; // sorted by start, seconds since midnight
+   int activeWindowIdx_;                           // index of currently scheduled/active window
 
-   bool LoadFromSettings(const WMSettings& settings);
+   bool LoadFromSettings(WMSettings& settings);
+   int FindActiveWindowIdx() const;
+   int FindNextWindowIdx() const;
 };
index dbaa51d1298bd4848d640bf68c5fc184c684cf70..9dd17250b285b32bd381367f0feaa7a309e18c4a 100644 (file)
@@ -96,6 +96,57 @@ static void FreeListViewEntries(HWND hListView)
    }
 }
 
+static bool TimeInWindow(DWORD t, DWORD wStart, DWORD wEnd) noexcept
+{
+   if (wStart < wEnd) {
+      return t >= wStart && t < wEnd;
+   } else { // wraps midnight
+      return t >= wStart || t < wEnd;
+   }
+}
+
+static bool WindowsOverlap(DWORD s1, DWORD e1, DWORD s2, DWORD e2) noexcept
+{
+   // Two circular intervals overlap iff one contains the start of the other
+   return TimeInWindow(s1, s2, e2) || TimeInWindow(s2, s1, e1);
+}
+
+static bool ShowOverlapError(HWND hParent)
+{
+   WMi18n &i18n = WMi18n::GetInstance();
+   TaskDialog(
+      hParent,
+      nullptr,
+      PROGRAM_NAME,
+      i18n.GetTranslationW("settings.quiet-hours.error.overlapping-time-range.title").c_str(),
+      i18n.GetTranslationW("settings.quiet-hours.error.overlapping-time-range.text").c_str(),
+      TDCBF_OK_BUTTON,
+      TD_WARNING_ICON,
+      nullptr);
+   return false;
+}
+
+static bool HasOverlapWithListView(
+   HWND hListView,
+   const QuietHoursEntry* newEntry,
+   int excludeIdx)
+{
+   const int count = ListView_GetItemCount(hListView);
+   for (int i = 0; i < count; ++i) {
+      if (i == excludeIdx) continue;
+      LVITEM lvi{};
+      lvi.mask  = LVIF_PARAM;
+      lvi.iItem = i;
+      if (ListView_GetItem(hListView, &lvi) && lvi.lParam != 0) {
+         const auto* existing = reinterpret_cast<const QuietHoursEntry*>(lvi.lParam);
+         if (WindowsOverlap(newEntry->start, newEntry->end, existing->start, existing->end)) {
+            return true;
+         }
+      }
+   }
+   return false;
+}
+
 static bool SaveQuietHours(
    WMSettings* settings,
    const int enabled,
@@ -383,8 +434,13 @@ INT_PTR CALLBACK Settings_QuietHoursDlgProc(HWND hDlg, UINT msg, WPARAM wParam,
                hDlg,
                Settings_QuietHoursAddDlgProc,
                reinterpret_cast<LPARAM>(qh_entry)) == 0) {
-            AddQuietHoursEntryToListView(hQuietHoursTimes, qh_entry);
-            UpdateButtonStates(hDlg, hQuietHoursTimes, true);
+            if (HasOverlapWithListView(hQuietHoursTimes, qh_entry, -1)) {
+               ShowOverlapError(hDlg);
+               delete qh_entry;
+            } else {
+               AddQuietHoursEntryToListView(hQuietHoursTimes, qh_entry);
+               UpdateButtonStates(hDlg, hQuietHoursTimes, true);
+            }
          } else {
             delete qh_entry;
          }
@@ -404,9 +460,13 @@ INT_PTR CALLBACK Settings_QuietHoursDlgProc(HWND hDlg, UINT msg, WPARAM wParam,
                      hDlg,
                      Settings_QuietHoursAddDlgProc,
                      reinterpret_cast<LPARAM>(&editCopy)) == 0) {
-                  entry->start = editCopy.start;
-                  entry->end   = editCopy.end;
-                  UpdateListViewItem(hQuietHoursTimes, sel, entry);
+                  if (HasOverlapWithListView(hQuietHoursTimes, &editCopy, sel)) {
+                     ShowOverlapError(hDlg);
+                  } else {
+                     entry->start = editCopy.start;
+                     entry->end   = editCopy.end;
+                     UpdateListViewItem(hQuietHoursTimes, sel, entry);
+                  }
                }
             }
          }
index 17c621a239139feecea5461e8598ec7468e7c12b..40e649f561db773deafc15a5a3bb421b74f18cdf 100644 (file)
@@ -173,7 +173,7 @@ void VistaAudio::Uninit()
       deviceEnumerator_->UnregisterEndpointNotificationCallback(mmnAudioEvents_);
    }
    deviceEnumerator_.Release();
-   SafeRelease(&mmnAudioEvents_);
+   mmnAudioEvents_.Release();
 
    endpoints_.clear();
 }