Skip to content

Commit fb75797

Browse files
committed
feat: ui 개선
1 parent e8ff12a commit fb75797

5 files changed

Lines changed: 154 additions & 88 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ set(SOURCES
1212
src/file_watcher.cpp
1313
src/wakatime_client.cpp
1414
src/tray_icon.cpp
15+
src/windows_dark_mode.cpp
1516
src/unity_focus_detector.cpp
1617
)
1718

@@ -21,6 +22,7 @@ set(HEADERS
2122
include/file_watcher.h
2223
include/wakatime_client.h
2324
include/tray_icon.h
25+
include/windows_dark_mode.h
2426
include/unity_focus_detector.h
2527
)
2628

@@ -52,6 +54,7 @@ if(WIN32)
5254
ole32
5355
oleaut32
5456
windowscodecs
57+
dwmapi
5558
user32
5659
)
5760
else()
@@ -64,6 +67,7 @@ if(WIN32)
6467
ole32
6568
oleaut32
6669
windowscodecs
70+
dwmapi
6771
user32
6872
)
6973
endif()

include/windows_dark_mode.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#pragma once
2+
3+
#include <windows.h>
4+
5+
namespace WindowsDarkMode
6+
{
7+
/**
8+
* Enable dark-mode preference for Win32 menus on supported Windows versions.
9+
* Returns true when the API path is available and invoked.
10+
*/
11+
bool EnableForApp();
12+
13+
/**
14+
* Apply dark-mode attributes to a specific window when supported.
15+
* Safe to call on unsupported versions (no-op fallback).
16+
*/
17+
void ApplyToWindow(HWND hwnd);
18+
}

main.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "wakatime_client.h"
55
#include "tray_icon.h"
66
#include "unity_focus_detector.h"
7+
#include "windows_dark_mode.h"
78

89
WakaTimeClient *g_wakatimeClient = nullptr;
910
FileWatcher *g_fileWatcher = nullptr;
@@ -243,6 +244,9 @@ void InitialUnityProjectScan()
243244
int main()
244245
{
245246
std::cout << "[Main] Unity WakaTime Monitor Starting..." << std::endl;
247+
const bool darkModeAvailable = WindowsDarkMode::EnableForApp();
248+
std::cout << "[Main] Dark mode menu opt-in: "
249+
<< (darkModeAvailable ? "enabled" : "not available") << std::endl;
246250

247251
TrayIcon trayIcon;
248252
g_trayIcon = &trayIcon;

src/tray_icon.cpp

Lines changed: 27 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
#include "tray_icon.h"
1+
#include "tray_icon.h"
22
#include "file_watcher.h"
33
#include "wakatime_client.h"
4+
#include "windows_dark_mode.h"
45

56
#include <wincodec.h>
67
#include <comdef.h>
@@ -101,6 +102,8 @@ bool TrayIcon::CreateHiddenWindow()
101102
return false;
102103
}
103104

105+
WindowsDarkMode::ApplyToWindow(hwnd);
106+
104107
std::cout << "[TrayIcon] Hidden window created" << std::endl;
105108
return true;
106109
}
@@ -317,17 +320,12 @@ HMENU TrayIcon::CreateContextMenu()
317320
const HMENU menu = CreatePopupMenu();
318321

319322
HMENU statusSubMenu = CreateStatusSubMenu();
320-
AppendMenuW(menu, MF_STRING | MF_POPUP, (UINT_PTR) statusSubMenu, L"📊 Status");
323+
AppendMenuW(menu, MF_STRING | MF_POPUP, (UINT_PTR) statusSubMenu, L"Status");
321324
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
322325

323326
AppendMenuW(menu, MF_STRING, IDM_TOGGLE_MONITORING, L"Pause Monitoring");
324-
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
325-
326-
AppendMenuW(menu, MF_STRING, IDM_OPEN_DASHBOARD, L"Open WakaTime Dashboard");
327-
AppendMenuW(menu, MF_STRING, IDM_SETTINGS, L"🔑 Setup API Key");
328-
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
329-
330-
AppendMenuW(menu, MF_STRING, IDM_GITHUB, L"ℹ️ Unity WakaTime v1.0.2");
327+
AppendMenuW(menu, MF_STRING, IDM_OPEN_DASHBOARD, L"Open Dashboard");
328+
AppendMenuW(menu, MF_STRING, IDM_SETTINGS, L"Setup API Key");
331329
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
332330

333331
AppendMenuW(menu, MF_STRING, IDM_EXIT, L"Exit");
@@ -340,96 +338,37 @@ HMENU TrayIcon::CreateStatusSubMenu()
340338
{
341339
const HMENU subMenu = CreatePopupMenu();
342340

343-
// === API Key 상태 ===
344-
if (Globals::GetWakaTimeClient())
345-
{
346-
std::string maskedKey = Globals::GetWakaTimeClient()->GetMaskedApiKey();
347-
const std::wstring apiKeyInfo = L"🔑 API Key: " + std::wstring(maskedKey.begin(), maskedKey.end());
348-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, apiKeyInfo.c_str());
349-
}
350-
else
351-
{
352-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"🔑 API Key: Not configured");
353-
}
354-
355-
AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr);
356-
357-
// === 모니터링 상태 ===
358-
const std::wstring monitoringStatus = isMonitoring ? L"✅ Monitoring: Active" : L"⏸️ Monitoring: Paused";
341+
// Monitoring state
342+
const std::wstring monitoringStatus = isMonitoring ? L"Monitoring: Active" : L"Monitoring: Paused";
359343
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, monitoringStatus.c_str());
360344

361-
// === 현재 프로젝트 ===
345+
// Current project
362346
if (!currentProject.empty())
363347
{
364-
const std::wstring projectInfo = L"🎮 Current: " + std::wstring(currentProject.begin(), currentProject.end());
348+
const std::wstring projectInfo = L"Current Project: " + std::wstring(currentProject.begin(), currentProject.end());
365349
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, projectInfo.c_str());
366350
} else
367351
{
368-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"🎮 No Unity project detected");
352+
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"No Unity project detected");
369353
}
370354

371-
// === Heartbeat 통계 ===
372-
const std::wstring heartbeatInfo = L"💓 Total Heartbeats: " + std::to_wstring(totalHeartbeats);
373-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, heartbeatInfo.c_str());
374-
AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr);
375-
376-
// === WakaTime 통계 ===
377-
if (Globals::GetWakaTimeClient())
355+
// Heartbeat summary
356+
int sent = 0;
357+
int failed = 0;
358+
if (const auto *client = Globals::GetWakaTimeClient())
378359
{
379-
int sent, failed;
380-
Globals::GetWakaTimeClient()->GetStats(sent, failed);
381-
382-
const std::wstring sentInfo = L"📤 Sent: " + std::to_wstring(sent);
383-
const std::wstring failedInfo = L"❌ Failed: " + std::to_wstring(failed);
384-
385-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, sentInfo.c_str());
386-
if (failed > 0)
387-
{
388-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, failedInfo.c_str());
389-
}
390-
391-
// 성공률 계산
392-
if (sent + failed > 0)
393-
{
394-
const int successRate = (sent * 100) / (sent + failed);
395-
const std::wstring rateInfo = L"📊 Success Rate: " + std::to_wstring(successRate) + L"%";
396-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, rateInfo.c_str());
397-
}
360+
client->GetStats(sent, failed);
398361
}
399-
else
400-
{
401-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, L"⚠️ WakaTime client not initialized");
402-
}
403-
404-
AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr);
405362

406-
// === 파일 감시 상태 ===
407-
if (Globals::GetFileWatcher())
408-
{
409-
const size_t watchedCount = Globals::GetFileWatcher()->GetWatchedProjectCount();
410-
const std::wstring watchInfo = L"👁️ Watching: " + std::to_wstring(watchedCount) + L" projects";
411-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, watchInfo.c_str());
412-
413-
// 감시 중인 프로젝트 목록 (최대 3개)
414-
const auto& watchedProjects = Globals::GetFileWatcher()->GetWatchedProjects();
415-
for (size_t i = 0; i < std::min((size_t) 3, watchedProjects.size()); i++)
416-
{
417-
const auto& projectName = watchedProjects[i].projectName;
418-
std::wstring projectItem = L" 📁 " + std::wstring(projectName.begin(), projectName.end());
419-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, projectItem.c_str());
420-
}
421-
422-
if (watchedProjects.size() > 3)
423-
{
424-
const std::wstring moreInfo = L" ... and " + std::to_wstring(watchedProjects.size() - 3) + L" more";
425-
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, moreInfo.c_str());
426-
}
427-
}
363+
std::wstring heartbeatInfo = L"Heartbeats: " + std::to_wstring(totalHeartbeats) +
364+
L" (Sent: " + std::to_wstring(sent) +
365+
L", Failed: " + std::to_wstring(failed) + L")";
366+
AppendMenuW(subMenu, MF_STRING | MF_GRAYED, 0, heartbeatInfo.c_str());
428367

429368
AppendMenuW(subMenu, MF_SEPARATOR, 0, nullptr);
430369

431-
// === 액션 항목들 ===
432-
AppendMenuW(subMenu, MF_STRING, IDM_SHOW_STATUS, L"🔄 Refresh Status");
370+
// Actions
371+
AppendMenuW(subMenu, MF_STRING, IDM_SHOW_STATUS, L"Refresh Status");
433372

434373
return subMenu;
435374
}
@@ -449,7 +388,7 @@ void TrayIcon::UpdateContextMenu()
449388
// 새로운 서브메뉴 생성 및 삽입
450389
HMENU newStatusSubMenu = CreateStatusSubMenu();
451390
InsertMenuW(hMenu, 0, MF_BYPOSITION | MF_STRING | MF_POPUP,
452-
(UINT_PTR)newStatusSubMenu, L"📊 Status");
391+
(UINT_PTR)newStatusSubMenu, L"Status");
453392
}
454393

455394
void TrayIcon::OpenGitHubRepository() {
@@ -598,7 +537,7 @@ std::string TrayIcon::ShowApiKeyInputDialog()
598537

599538
const std::wstring wMessage(message.begin(), message.end());
600539

601-
if (const int result = MessageBoxW(hwnd, wMessage.c_str(), L"🔑 WakaTime API Key Setup",
540+
if (const int result = MessageBoxW(hwnd, wMessage.c_str(), L"WakaTime API Key Setup",
602541
MB_OKCANCEL | MB_ICONINFORMATION | MB_TOPMOST); result == IDOK)
603542
{
604543
if (const std::string clipboardText = GetClipboardText(); !clipboardText.empty())
@@ -607,13 +546,13 @@ std::string TrayIcon::ShowApiKeyInputDialog()
607546
}
608547
else
609548
{
610-
std::wstring retryMessage = L"No valid API key found in clipboard!\n\n";
549+
std::wstring retryMessage = L"No valid API key found in clipboard.\n\n";
611550
retryMessage += L"Please:\n";
612551
retryMessage += L"1. Go to the opened WakaTime page\n";
613552
retryMessage += L"2. Copy your API key\n";
614553
retryMessage += L"3. Try again from the tray menu\n\n";
615554

616-
MessageBoxW(hwnd, retryMessage.c_str(), L"⚠️ API Key Not Found",
555+
MessageBoxW(hwnd, retryMessage.c_str(), L"API Key Not Found",
617556
MB_OK | MB_ICONWARNING | MB_TOPMOST);
618557
}
619558
}

src/windows_dark_mode.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#include "windows_dark_mode.h"
2+
3+
#include <dwmapi.h>
4+
5+
namespace
6+
{
7+
enum class PreferredAppMode
8+
{
9+
Default,
10+
AllowDark,
11+
ForceDark,
12+
ForceLight,
13+
Max
14+
};
15+
16+
using AllowDarkModeForWindowFn = BOOL(WINAPI *)(HWND, BOOL);
17+
using SetPreferredAppModeFn = PreferredAppMode(WINAPI *)(PreferredAppMode);
18+
using FlushMenuThemesFn = void(WINAPI *)();
19+
20+
struct DarkModeApi
21+
{
22+
HMODULE uxTheme = nullptr;
23+
AllowDarkModeForWindowFn allowDarkModeForWindow = nullptr;
24+
SetPreferredAppModeFn setPreferredAppMode = nullptr;
25+
FlushMenuThemesFn flushMenuThemes = nullptr;
26+
bool loaded = false;
27+
};
28+
29+
DarkModeApi &GetDarkModeApi()
30+
{
31+
static DarkModeApi api;
32+
if (api.loaded)
33+
{
34+
return api;
35+
}
36+
37+
api.loaded = true;
38+
api.uxTheme = LoadLibraryW(L"uxtheme.dll");
39+
if (!api.uxTheme)
40+
{
41+
return api;
42+
}
43+
44+
api.allowDarkModeForWindow = reinterpret_cast<AllowDarkModeForWindowFn>(
45+
GetProcAddress(api.uxTheme, MAKEINTRESOURCEA(133)));
46+
api.setPreferredAppMode = reinterpret_cast<SetPreferredAppModeFn>(
47+
GetProcAddress(api.uxTheme, MAKEINTRESOURCEA(135)));
48+
api.flushMenuThemes = reinterpret_cast<FlushMenuThemesFn>(
49+
GetProcAddress(api.uxTheme, MAKEINTRESOURCEA(136)));
50+
51+
return api;
52+
}
53+
}
54+
55+
bool WindowsDarkMode::EnableForApp()
56+
{
57+
auto &api = GetDarkModeApi();
58+
if (!api.setPreferredAppMode)
59+
{
60+
return false;
61+
}
62+
63+
api.setPreferredAppMode(PreferredAppMode::AllowDark);
64+
if (api.flushMenuThemes)
65+
{
66+
api.flushMenuThemes();
67+
}
68+
69+
return true;
70+
}
71+
72+
void WindowsDarkMode::ApplyToWindow(const HWND hwnd)
73+
{
74+
if (!hwnd)
75+
{
76+
return;
77+
}
78+
79+
auto &api = GetDarkModeApi();
80+
if (api.allowDarkModeForWindow)
81+
{
82+
api.allowDarkModeForWindow(hwnd, TRUE);
83+
}
84+
85+
BOOL useDark = TRUE;
86+
87+
constexpr DWORD kDwmwaUseImmersiveDarkMode = 20;
88+
HRESULT hr = DwmSetWindowAttribute(hwnd,
89+
kDwmwaUseImmersiveDarkMode,
90+
&useDark,
91+
sizeof(useDark));
92+
if (FAILED(hr))
93+
{
94+
// Older Windows 10 builds used attribute id 19 for immersive dark mode.
95+
constexpr DWORD kDwmwaUseImmersiveDarkModeLegacy = 19;
96+
DwmSetWindowAttribute(hwnd,
97+
kDwmwaUseImmersiveDarkModeLegacy,
98+
&useDark,
99+
sizeof(useDark));
100+
}
101+
}

0 commit comments

Comments
 (0)