diff --git a/src/AxCopilot/Services/WeatherWidgetService.cs b/src/AxCopilot/Services/WeatherWidgetService.cs new file mode 100644 index 0000000..3de0d35 --- /dev/null +++ b/src/AxCopilot/Services/WeatherWidgetService.cs @@ -0,0 +1,63 @@ +using System.Net.Http; + +namespace AxCopilot.Services; + +/// +/// 날씨 위젯용 데이터 서비스. +/// 사외 모드에서만 외부 API(wttr.in)를 호출하며 30분간 결과를 캐시합니다. +/// 사내 모드(InternalModeEnabled=true)에서는 "--"를 즉시 반환합니다. +/// +internal static class WeatherWidgetService +{ + private static string? _cached; + private static DateTime _cacheTime = DateTime.MinValue; + private static bool _fetching; + private static readonly TimeSpan _ttl = TimeSpan.FromMinutes(30); + + /// 현재 캐시된 날씨 텍스트 (없으면 "--") + public static string CachedText => _cached ?? "--"; + + /// + /// 날씨 정보를 비동기로 갱신합니다. + /// 사내 모드이거나 캐시 유효 기간 내이면 즉시 반환합니다. + /// + public static async Task RefreshAsync(bool internalMode) + { + if (internalMode) + { + _cached = "--"; + return; + } + + // 캐시 유효하면 재호출 불필요 + if (_cached != null && DateTime.Now - _cacheTime < _ttl) return; + if (_fetching) return; + + _fetching = true; + try + { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(6); + // wttr.in: %c = 날씨 조건 이모지, %t = 기온 + var raw = await client.GetStringAsync("https://wttr.in/?format=%c+%t"); + _cached = raw.Trim().Replace("+", " "); + _cacheTime = DateTime.Now; + } + catch (Exception ex) + { + LogService.Warn($"날씨 위젯 갱신 실패: {ex.Message}"); + _cached ??= "--"; + } + finally + { + _fetching = false; + } + } + + /// 캐시를 강제 초기화하여 다음 호출 시 재조회합니다. + public static void Invalidate() + { + _cached = null; + _cacheTime = DateTime.MinValue; + } +} diff --git a/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs b/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs index 75e610a..befc388 100644 --- a/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs +++ b/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs @@ -57,6 +57,51 @@ public partial class LauncherViewModel public bool Widget_McpOnline => ServerStatusService.Instance.McpOnline; public string Widget_McpName => ServerStatusService.Instance.McpName; + // ─── 날씨 ───────────────────────────────────────────────────────────────── + + private string _widget_WeatherText = "--"; + + /// 날씨 위젯 텍스트 예: "⛅ 18°C" (사외 모드) / "--" (사내 모드) + public string Widget_WeatherText + { + get => _widget_WeatherText; + internal set { _widget_WeatherText = value; OnPropertyChanged(); } + } + + // ─── 날짜/일정 ──────────────────────────────────────────────────────────── + + /// 날짜 위젯 텍스트 예: "4/4 (화)" + public string Widget_CalText => + DateTime.Now.ToString("M/d (ddd)", + System.Globalization.CultureInfo.GetCultureInfo("ko-KR")); + + // ─── 배터리 ─────────────────────────────────────────────────────────────── + + private string _widget_BatteryText = "--"; + private string _widget_BatteryIcon = "\uE83F"; + private bool _widget_BatteryVisible = false; + + /// 배터리 퍼센트 및 충전 표시. 예: "87%" / "45% ⚡" + public string Widget_BatteryText + { + get => _widget_BatteryText; + internal set { _widget_BatteryText = value; OnPropertyChanged(); } + } + + /// 배터리 잔량별 Segoe MDL2 아이콘 코드포인트 + public string Widget_BatteryIcon + { + get => _widget_BatteryIcon; + internal set { _widget_BatteryIcon = value; OnPropertyChanged(); } + } + + /// 배터리 위젯 표시 여부 (데스크톱/배터리 없음이면 false) + public bool Widget_BatteryVisible + { + get => _widget_BatteryVisible; + internal set { _widget_BatteryVisible = value; OnPropertyChanged(); } + } + // ─── 갱신 메서드 ────────────────────────────────────────────────────────── /// 1초마다 LauncherWindow.Widgets.cs에서 호출 — UI 바인딩 갱신. @@ -75,6 +120,9 @@ public partial class LauncherViewModel OnPropertyChanged(nameof(Widget_LlmOnline)); OnPropertyChanged(nameof(Widget_McpOnline)); OnPropertyChanged(nameof(Widget_McpName)); + OnPropertyChanged(nameof(Widget_CalText)); + // WeatherText / BatteryText / BatteryIcon / BatteryVisible 는 + // LauncherWindow.Widgets.cs에서 직접 setter 호출로 갱신 } private int _widgetRefreshTick; diff --git a/src/AxCopilot/Views/LauncherWindow.Widgets.cs b/src/AxCopilot/Views/LauncherWindow.Widgets.cs index 9d75e30..52d01c5 100644 --- a/src/AxCopilot/Views/LauncherWindow.Widgets.cs +++ b/src/AxCopilot/Views/LauncherWindow.Widgets.cs @@ -45,6 +45,9 @@ public partial class LauncherWindow // 메모 건수 즉시 갱신 (최초 1회) _vm.UpdateWidgets(); UpdateServerDots(); + UpdateBatteryWidget(); + // 날씨: 사외 모드에서만 비동기 갱신 + _ = RefreshWeatherAsync(); // 1초 타이머 if (_widgetTimer == null) @@ -57,6 +60,14 @@ public partial class LauncherWindow { _vm.UpdateWidgets(); UpdateServerDots(); + + // 배터리: 30초마다 갱신 + if (_vm.Widget_PerfText.Length > 0 && _widgetBatteryTick++ % 30 == 0) + UpdateBatteryWidget(); + + // 날씨: 2분마다 서비스 쪽 캐시를 체크 (실제 API 호출은 30분마다) + if (_widgetWeatherTick++ % 120 == 0) + _ = RefreshWeatherAsync(); }; } _widgetTimer.Start(); @@ -65,6 +76,9 @@ public partial class LauncherWindow UpdatePomoWidgetStyle(); } + private int _widgetBatteryTick; + private int _widgetWeatherTick; + /// 런처가 숨겨질 때 타이머 중지 (뽀모도로는 계속 실행). internal void StopWidgetUpdates() { @@ -140,4 +154,112 @@ public partial class LauncherWindow _vm.InputText = "port"; InputBox?.Focus(); } + + private void WgtWeather_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true; + if (!internalMode) + { + // 사외 모드: 브라우저에서 날씨 페이지 열기 + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo( + "https://wttr.in") { UseShellExecute = true }); + } + catch (Exception ex) + { + LogService.Warn($"날씨 링크 열기 실패: {ex.Message}"); + } + } + else + { + // 사내 모드: 날씨 캐시 무효화 후 재시도 (사외 모드 전환 시 즉시 갱신되도록) + WeatherWidgetService.Invalidate(); + _ = RefreshWeatherAsync(); + } + } + + private void WgtCal_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + // Windows 달력 앱 열기 + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo( + "ms-clock:") { UseShellExecute = true }); + } + catch + { + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo( + "outlookcal:") { UseShellExecute = true }); + } + catch (Exception ex) + { + LogService.Warn($"달력 앱 열기 실패: {ex.Message}"); + } + } + } + + private void WgtBattery_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + // 전원 및 절전 설정 열기 + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo( + "ms-settings:powersleep") { UseShellExecute = true }); + } + catch (Exception ex) + { + LogService.Warn($"전원 설정 열기 실패: {ex.Message}"); + } + } + + // ─── 배터리 상태 갱신 ────────────────────────────────────────────────────── + + private void UpdateBatteryWidget() + { + try + { + var ps = System.Windows.Forms.SystemInformation.PowerStatus; + var pct = ps.BatteryLifePercent; // 0.0~1.0, unknown = 255f + + if (pct > 1.0f || pct < 0f) + { + // 배터리 없는 장치 (데스크톱 등) → 위젯 숨김 + _vm.Widget_BatteryVisible = false; + return; + } + + _vm.Widget_BatteryVisible = true; + var pctInt = (int)(pct * 100); + var charging = ps.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online; + + _vm.Widget_BatteryText = charging ? $"{pctInt}% ⚡" : $"{pctInt}%"; + _vm.Widget_BatteryIcon = charging ? "\uE83E" // BatteryCharging + : pctInt >= 85 ? "\uEBA7" // Battery 8 (Full) + : pctInt >= 70 ? "\uEBA5" // Battery 6 + : pctInt >= 50 ? "\uEBA3" // Battery 4 + : pctInt >= 25 ? "\uEBA1" // Battery 2 + : "\uEBA0"; // Battery 1 (Low) + } + catch (Exception ex) + { + LogService.Warn($"배터리 위젯 갱신 실패: {ex.Message}"); + _vm.Widget_BatteryVisible = false; + } + } + + // ─── 날씨 비동기 갱신 ───────────────────────────────────────────────────── + + private async Task RefreshWeatherAsync() + { + var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true; + await WeatherWidgetService.RefreshAsync(internalMode); + // UI 스레드에서 바인딩 프로퍼티 갱신 + await Dispatcher.InvokeAsync(() => + { + _vm.Widget_WeatherText = WeatherWidgetService.CachedText; + }); + } } diff --git a/src/AxCopilot/Views/LauncherWindow.xaml b/src/AxCopilot/Views/LauncherWindow.xaml index 1b3ea24..69a5953 100644 --- a/src/AxCopilot/Views/LauncherWindow.xaml +++ b/src/AxCopilot/Views/LauncherWindow.xaml @@ -790,116 +790,189 @@ Margin="0,0,0,8" Opacity="0.7"/> - + - - - - - - - - - - + - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +