mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-26 20:24:26 +08:00
feat.完善了时钟轻应用,为启动器提供了多语言支持
This commit is contained in:
@@ -1,64 +1,16 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LanMountainDesktop.AirAppHost.AirAppWindow"
|
||||
Width="520"
|
||||
Height="360"
|
||||
MinWidth="360"
|
||||
MinHeight="260"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowDecorations="None"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
TransparencyLevelHint="Transparent"
|
||||
Background="Transparent"
|
||||
FontFamily="{DynamicResource AppFontFamily}"
|
||||
Title="Air APP">
|
||||
<Border x:Name="WindowShell"
|
||||
Background="{DynamicResource AirAppWindowBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource AirAppWindowBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
ClipToBounds="True"
|
||||
BoxShadow="0 18 44 #22000000">
|
||||
<Grid RowDefinitions="52,*">
|
||||
<Grid x:Name="TitleBar"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Background="Transparent"
|
||||
PointerPressed="OnTitleBarPointerPressed">
|
||||
<StackPanel Margin="18,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Text="Air APP"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<TextBlock x:Name="SubtitleTextBlock"
|
||||
Text="LanMountainDesktop"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AirAppSecondaryTextBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Margin="0,8,10,8"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCloseClick">
|
||||
<TextBlock Text="X"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ContentControl x:Name="ContentHost"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
<faWindowing:FAAppWindow xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:faWindowing="using:FluentAvalonia.UI.Windowing"
|
||||
x:Class="LanMountainDesktop.AirAppHost.AirAppWindow"
|
||||
Width="520"
|
||||
Height="360"
|
||||
MinWidth="360"
|
||||
MinHeight="260"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
FontFamily="{DynamicResource AppFontFamily}"
|
||||
Title="Air APP">
|
||||
<Grid x:Name="WindowRoot"
|
||||
Background="{DynamicResource AirAppWindowBackgroundBrush}">
|
||||
<ContentControl x:Name="ContentHost" />
|
||||
</Grid>
|
||||
</faWindowing:FAAppWindow>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Windowing;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
@@ -10,7 +10,7 @@ using LanMountainDesktop.Views.Components;
|
||||
|
||||
namespace LanMountainDesktop.AirAppHost;
|
||||
|
||||
public sealed partial class AirAppWindow : Window
|
||||
public sealed partial class AirAppWindow : FAAppWindow
|
||||
{
|
||||
private readonly AirAppLaunchOptions _options;
|
||||
private readonly AirAppWindowDescriptor _descriptor;
|
||||
@@ -36,7 +36,7 @@ public sealed partial class AirAppWindow : Window
|
||||
|
||||
if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ContentHost.Content = new WorldClockAirAppView(_options);
|
||||
ContentHost.Content = new ClockAirAppView(_options);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,41 +56,41 @@ public sealed partial class AirAppWindow : Window
|
||||
private void ApplyWindowDescriptor(AirAppWindowDescriptor descriptor)
|
||||
{
|
||||
Title = descriptor.Title;
|
||||
TitleTextBlock.Text = descriptor.TitleText;
|
||||
SubtitleTextBlock.Text = descriptor.SubtitleText;
|
||||
Width = descriptor.Width;
|
||||
Height = descriptor.Height;
|
||||
MinWidth = descriptor.MinWidth;
|
||||
MinHeight = descriptor.MinHeight;
|
||||
ShowInTaskbar = descriptor.ShowInTaskbar;
|
||||
CanResize = descriptor.CanResize;
|
||||
WindowDecorations = WindowDecorations.None;
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaTitleBarHeightHint = -1;
|
||||
|
||||
TitleBar.IsVisible = true;
|
||||
Grid.SetRow(ContentHost, 1);
|
||||
Grid.SetRowSpan(ContentHost, 1);
|
||||
ShowAsDialog = descriptor.ShowAsDialog;
|
||||
WindowState = WindowState.Normal;
|
||||
WindowRoot.Background = this.TryFindResource("AirAppWindowBackgroundBrush", out var brush) && brush is IBrush backgroundBrush
|
||||
? backgroundBrush
|
||||
: Brushes.White;
|
||||
ConfigureTitleBar(descriptor);
|
||||
|
||||
switch (descriptor.ChromeMode)
|
||||
{
|
||||
case AirAppWindowChromeMode.Standard:
|
||||
WindowDecorations = WindowDecorations.Full;
|
||||
TitleBar.ExtendsContentIntoTitleBar = false;
|
||||
break;
|
||||
|
||||
case AirAppWindowChromeMode.Borderless:
|
||||
HideCustomTitleBar();
|
||||
WindowDecorations = WindowDecorations.None;
|
||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
break;
|
||||
|
||||
case AirAppWindowChromeMode.FullScreen:
|
||||
HideCustomTitleBar();
|
||||
WindowShell.CornerRadius = new Avalonia.CornerRadius(0);
|
||||
WindowShell.BorderThickness = new Avalonia.Thickness(0);
|
||||
WindowShell.BoxShadow = default;
|
||||
WindowDecorations = WindowDecorations.None;
|
||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
ShowAsDialog = false;
|
||||
WindowState = WindowState.FullScreen;
|
||||
break;
|
||||
|
||||
case AirAppWindowChromeMode.Tool:
|
||||
WindowDecorations = WindowDecorations.Full;
|
||||
TitleBar.ExtendsContentIntoTitleBar = false;
|
||||
ShowInTaskbar = false;
|
||||
CanResize = false;
|
||||
break;
|
||||
@@ -102,11 +102,18 @@ public sealed partial class AirAppWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void HideCustomTitleBar()
|
||||
private void ConfigureTitleBar(AirAppWindowDescriptor descriptor)
|
||||
{
|
||||
TitleBar.IsVisible = false;
|
||||
Grid.SetRow(ContentHost, 0);
|
||||
Grid.SetRowSpan(ContentHost, 2);
|
||||
TitleBar.Height = descriptor.ChromeMode == AirAppWindowChromeMode.Tool ? 36 : 40;
|
||||
TitleBar.BackgroundColor = Colors.Transparent;
|
||||
TitleBar.ForegroundColor = Color.FromRgb(32, 32, 32);
|
||||
TitleBar.InactiveBackgroundColor = Colors.Transparent;
|
||||
TitleBar.InactiveForegroundColor = Color.FromRgb(96, 96, 96);
|
||||
TitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(23, 0, 0, 0);
|
||||
TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(52, 0, 0, 0);
|
||||
TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||
TitleBar.ButtonInactiveForegroundColor = Colors.Gray;
|
||||
}
|
||||
|
||||
private void ConfigureWhiteboardWindow()
|
||||
@@ -147,6 +154,12 @@ public sealed partial class AirAppWindow : Window
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
SaveWhiteboard();
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
SaveAndDisposeWhiteboard();
|
||||
@@ -154,20 +167,6 @@ public sealed partial class AirAppWindow : Window
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
SaveWhiteboard();
|
||||
Close();
|
||||
}
|
||||
|
||||
private void SaveAndDisposeWhiteboard()
|
||||
{
|
||||
var widget = _whiteboardWidget;
|
||||
@@ -259,6 +258,11 @@ public sealed partial class AirAppWindow : Window
|
||||
return _options.InstanceKey.Trim();
|
||||
}
|
||||
|
||||
if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{AirAppLaunchOptions.WorldClockAppId}:clock-suite:global";
|
||||
}
|
||||
|
||||
var componentId = string.IsNullOrWhiteSpace(_options.SourceComponentId)
|
||||
? "none"
|
||||
: _options.SourceComponentId.Trim();
|
||||
|
||||
@@ -7,6 +7,7 @@ public sealed record AirAppWindowDescriptor(
|
||||
AirAppWindowChromeMode ChromeMode,
|
||||
bool CanResize,
|
||||
bool ShowInTaskbar,
|
||||
bool ShowAsDialog,
|
||||
double Width,
|
||||
double Height,
|
||||
double MinWidth,
|
||||
@@ -23,13 +24,15 @@ public sealed record AirAppWindowDescriptor(
|
||||
if (string.Equals(options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Standard(
|
||||
"World Clock - Air APP",
|
||||
"World Clock",
|
||||
"Clock - Air APP",
|
||||
"Clock",
|
||||
"Air APP",
|
||||
width: 360,
|
||||
height: 220,
|
||||
minWidth: 320,
|
||||
minHeight: 220);
|
||||
width: 780,
|
||||
height: 560,
|
||||
minWidth: 680,
|
||||
minHeight: 480,
|
||||
canResize: true,
|
||||
showAsDialog: false);
|
||||
}
|
||||
|
||||
if (string.Equals(options.AppId, AirAppLaunchOptions.WhiteboardAppId, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -53,15 +56,18 @@ public sealed record AirAppWindowDescriptor(
|
||||
double width = 520,
|
||||
double height = 360,
|
||||
double minWidth = 360,
|
||||
double minHeight = 260)
|
||||
double minHeight = 260,
|
||||
bool canResize = true,
|
||||
bool showAsDialog = false)
|
||||
{
|
||||
return new AirAppWindowDescriptor(
|
||||
windowTitle,
|
||||
titleBarTitle,
|
||||
titleBarSubtitle,
|
||||
AirAppWindowChromeMode.Standard,
|
||||
CanResize: true,
|
||||
CanResize: canResize,
|
||||
ShowInTaskbar: true,
|
||||
ShowAsDialog: showAsDialog,
|
||||
width,
|
||||
height,
|
||||
minWidth,
|
||||
@@ -80,6 +86,7 @@ public sealed record AirAppWindowDescriptor(
|
||||
AirAppWindowChromeMode.FullScreen,
|
||||
CanResize: false,
|
||||
ShowInTaskbar: true,
|
||||
ShowAsDialog: false,
|
||||
Width: 1280,
|
||||
Height: 720,
|
||||
MinWidth: 360,
|
||||
@@ -98,6 +105,7 @@ public sealed record AirAppWindowDescriptor(
|
||||
AirAppWindowChromeMode.Borderless,
|
||||
CanResize: true,
|
||||
ShowInTaskbar: true,
|
||||
ShowAsDialog: false,
|
||||
width,
|
||||
height,
|
||||
MinWidth: 240,
|
||||
@@ -118,6 +126,7 @@ public sealed record AirAppWindowDescriptor(
|
||||
AirAppWindowChromeMode.Tool,
|
||||
CanResize: false,
|
||||
ShowInTaskbar: false,
|
||||
ShowAsDialog: true,
|
||||
width,
|
||||
height,
|
||||
MinWidth: 240,
|
||||
@@ -133,6 +142,7 @@ public sealed record AirAppWindowDescriptor(
|
||||
AirAppWindowChromeMode.BackgroundOnly,
|
||||
CanResize: false,
|
||||
ShowInTaskbar: false,
|
||||
ShowAsDialog: false,
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
MinWidth: 1,
|
||||
|
||||
310
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml
Normal file
310
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml
Normal file
@@ -0,0 +1,310 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LanMountainDesktop.AirAppHost.ClockAirAppView">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.clock-card">
|
||||
<Setter Property="Background" Value="#F7FFFFFF" />
|
||||
<Setter Property="BorderBrush" Value="#16000000" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="18" />
|
||||
<Setter Property="Padding" Value="18" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.clock-tab">
|
||||
<Setter Property="MinWidth" Value="84" />
|
||||
<Setter Property="Height" Value="34" />
|
||||
<Setter Property="Padding" Value="14,0" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AirAppSecondaryTextBrush}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.clock-tab:checked">
|
||||
<Setter Property="Background" Value="{DynamicResource AirAppAccentBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.clock-command">
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
<Setter Property="Padding" Value="14,6" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.clock-icon-command">
|
||||
<Setter Property="Width" Value="34" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.clock-muted">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AirAppSecondaryTextBrush}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="16"
|
||||
Margin="22,14,22,20">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Spacing="3"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="HeaderTitleTextBlock"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<TextBlock x:Name="HeaderSubtitleTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="12" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Background="#0A000000"
|
||||
CornerRadius="14"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="4">
|
||||
<ToggleButton x:Name="WorldTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="world"
|
||||
Click="OnTabButtonClick" />
|
||||
<ToggleButton x:Name="StopwatchTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="stopwatch"
|
||||
Click="OnTabButtonClick" />
|
||||
<ToggleButton x:Name="TimerTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="timer"
|
||||
Click="OnTabButtonClick" />
|
||||
<ToggleButton x:Name="SettingsTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="settings"
|
||||
Click="OnTabButtonClick" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid x:Name="WorldPage"
|
||||
ColumnDefinitions="1.05*,1.1*"
|
||||
ColumnSpacing="16">
|
||||
<Border Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock x:Name="LocalLabelTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold" />
|
||||
<StackPanel Grid.Row="1"
|
||||
Spacing="12"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LocalTimeTextBlock"
|
||||
FontSize="54"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="0"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<TextBlock x:Name="LocalDateTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="15" />
|
||||
<TextBlock x:Name="LocalTimeZoneTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="WorldSummaryTextBlock"
|
||||
Grid.Row="2"
|
||||
Classes="clock-muted"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="12">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<TextBox x:Name="TimeZoneSearchTextBox"
|
||||
PlaceholderText="Search"
|
||||
TextChanged="OnTimeZoneSearchChanged" />
|
||||
<Button x:Name="AddCityButton"
|
||||
Grid.Column="1"
|
||||
Classes="clock-command"
|
||||
Click="OnAddCityClick" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox x:Name="TimeZoneComboBox"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxDropDownHeight="280" />
|
||||
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="WorldClockRowsPanel"
|
||||
Spacing="8" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="StopwatchPage"
|
||||
Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="18">
|
||||
<TextBlock x:Name="StopwatchHintTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="13" />
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Spacing="18"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock x:Name="StopwatchElapsedTextBlock"
|
||||
Text="00:00:00.00"
|
||||
FontSize="58"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="0"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="10">
|
||||
<Button x:Name="StopwatchStartPauseButton"
|
||||
Classes="clock-command"
|
||||
Click="OnStopwatchStartPauseClick" />
|
||||
<Button x:Name="StopwatchLapButton"
|
||||
Classes="clock-command"
|
||||
Click="OnStopwatchLapClick" />
|
||||
<Button x:Name="StopwatchResetButton"
|
||||
Classes="clock-command"
|
||||
Click="OnStopwatchResetClick" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="StopwatchLapsPanel"
|
||||
Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="TimerPage"
|
||||
Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*"
|
||||
RowSpacing="18">
|
||||
<TextBlock x:Name="TimerHintTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="13" />
|
||||
|
||||
<TextBlock x:Name="TimerRemainingTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="00:05:00"
|
||||
FontSize="58"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="0"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button Classes="clock-command"
|
||||
Tag="1"
|
||||
Click="OnTimerPresetClick">1</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="5"
|
||||
Click="OnTimerPresetClick">5</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="10"
|
||||
Click="OnTimerPresetClick">10</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="15"
|
||||
Click="OnTimerPresetClick">15</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="30"
|
||||
Click="OnTimerPresetClick">30</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="3"
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
RowSpacing="14"
|
||||
HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBox x:Name="TimerMinutesTextBox"
|
||||
Width="120"
|
||||
PlaceholderText="Minutes"
|
||||
Text="5" />
|
||||
<Button x:Name="TimerApplyButton"
|
||||
Classes="clock-command"
|
||||
Click="OnTimerApplyClick" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="10">
|
||||
<Button x:Name="TimerStartPauseButton"
|
||||
Classes="clock-command"
|
||||
Click="OnTimerStartPauseClick" />
|
||||
<Button x:Name="TimerResetButton"
|
||||
Classes="clock-command"
|
||||
Click="OnTimerResetClick" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="TimerStatusTextBlock"
|
||||
Grid.Row="2"
|
||||
Classes="clock-muted"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="SettingsPage"
|
||||
Classes="clock-card">
|
||||
<StackPanel Spacing="18"
|
||||
MaxWidth="560"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock x:Name="SettingsHeaderTextBlock"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
|
||||
<Grid ColumnDefinitions="220,*"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto"
|
||||
RowSpacing="14"
|
||||
ColumnSpacing="18">
|
||||
<TextBlock x:Name="TimeFormatLabelTextBlock"
|
||||
Classes="clock-muted"
|
||||
VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="TimeFormatComboBox"
|
||||
Grid.Column="1"
|
||||
SelectionChanged="OnSettingsChanged" />
|
||||
|
||||
<TextBlock x:Name="StartupTabLabelTextBlock"
|
||||
Grid.Row="1"
|
||||
Classes="clock-muted"
|
||||
VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="StartupTabComboBox"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
SelectionChanged="OnSettingsChanged" />
|
||||
|
||||
<CheckBox x:Name="ShowSecondsCheckBox"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
IsCheckedChanged="OnSettingsChanged" />
|
||||
<CheckBox x:Name="ActivateOnTimerFinishedCheckBox"
|
||||
Grid.Row="3"
|
||||
Grid.ColumnSpan="2"
|
||||
IsCheckedChanged="OnSettingsChanged" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
665
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml.cs
Normal file
665
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml.cs
Normal file
@@ -0,0 +1,665 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
namespace LanMountainDesktop.AirAppHost;
|
||||
|
||||
public sealed partial class ClockAirAppView : UserControl
|
||||
{
|
||||
private sealed class WorldClockRowVisual
|
||||
{
|
||||
public required TimeZoneInfo TimeZone { get; init; }
|
||||
|
||||
public required TextBlock TimeTextBlock { get; init; }
|
||||
|
||||
public required TextBlock DateTextBlock { get; init; }
|
||||
|
||||
public required TextBlock OffsetTextBlock { get; init; }
|
||||
}
|
||||
|
||||
private readonly DispatcherTimer _clockTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(250)
|
||||
};
|
||||
|
||||
private readonly AirAppLaunchOptions _options;
|
||||
private readonly ClockAirAppSettingsStore _settingsStore = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly ClockAirAppStopwatchState _stopwatchState = new();
|
||||
private readonly ClockAirAppTimerState _timerState = new();
|
||||
private readonly List<TimeZoneInfo> _allTimeZones;
|
||||
private readonly List<WorldClockRowVisual> _worldClockRows = [];
|
||||
|
||||
private ClockAirAppSettingsSnapshot _settings = ClockAirAppSettingsSnapshot.Normalize(null);
|
||||
private CultureInfo _culture = CultureInfo.CurrentCulture;
|
||||
private string _languageCode = "zh-CN";
|
||||
private string _selectedTab = ClockAirAppTabIds.WorldClock;
|
||||
private bool _suppressSettingsEvents;
|
||||
|
||||
public ClockAirAppView()
|
||||
: this(AirAppLaunchOptions.Parse([]))
|
||||
{
|
||||
}
|
||||
|
||||
public ClockAirAppView(AirAppLaunchOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_allTimeZones = TimeZoneInfo.GetSystemTimeZones()
|
||||
.OrderBy(static zone => zone.GetUtcOffset(DateTime.UtcNow))
|
||||
.ThenBy(static zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
InitializeComponent();
|
||||
LoadLanguage();
|
||||
LoadSettings();
|
||||
ApplyLocalizedText();
|
||||
PopulateSettingsControls();
|
||||
PopulateTimeZoneCombo();
|
||||
RebuildWorldClockRows();
|
||||
SelectStartupTab();
|
||||
UpdateAll();
|
||||
|
||||
_clockTimer.Tick += OnClockTimerTick;
|
||||
AttachedToVisualTree += (_, _) =>
|
||||
{
|
||||
UpdateAll();
|
||||
_clockTimer.Start();
|
||||
};
|
||||
DetachedFromVisualTree += (_, _) => _clockTimer.Stop();
|
||||
}
|
||||
|
||||
private void LoadLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var appSettings = new AppSettingsService().Load();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
||||
_culture = CultureInfo.GetCultureInfo(_languageCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_languageCode = "zh-CN";
|
||||
_culture = CultureInfo.GetCultureInfo("zh-CN");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
_settings = _settingsStore.Load();
|
||||
_timerState.SetDuration(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
private void ApplyLocalizedText()
|
||||
{
|
||||
HeaderTitleTextBlock.Text = L("clockairapp.title", "Clock");
|
||||
HeaderSubtitleTextBlock.Text = L("clockairapp.subtitle", "World clock, stopwatch and timer");
|
||||
|
||||
WorldTabButton.Content = L("clockairapp.tab.world", "World");
|
||||
StopwatchTabButton.Content = L("clockairapp.tab.stopwatch", "Stopwatch");
|
||||
TimerTabButton.Content = L("clockairapp.tab.timer", "Timer");
|
||||
SettingsTabButton.Content = L("clockairapp.tab.settings", "Settings");
|
||||
|
||||
LocalLabelTextBlock.Text = L("clockairapp.world.local", "Local time");
|
||||
AddCityButton.Content = L("clockairapp.world.add", "Add");
|
||||
TimeZoneSearchTextBox.PlaceholderText = L("clockairapp.world.search", "Search city or time zone");
|
||||
|
||||
StopwatchHintTextBlock.Text = L("clockairapp.stopwatch.hint", "Lap timing stays in this window session.");
|
||||
StopwatchStartPauseButton.Content = L("clockairapp.action.start", "Start");
|
||||
StopwatchLapButton.Content = L("clockairapp.stopwatch.lap", "Lap");
|
||||
StopwatchResetButton.Content = L("clockairapp.action.reset", "Reset");
|
||||
|
||||
TimerHintTextBlock.Text = L("clockairapp.timer.hint", "Choose a preset or enter custom minutes.");
|
||||
TimerApplyButton.Content = L("clockairapp.timer.apply", "Apply");
|
||||
TimerStartPauseButton.Content = L("clockairapp.action.start", "Start");
|
||||
TimerResetButton.Content = L("clockairapp.action.reset", "Reset");
|
||||
TimerMinutesTextBox.PlaceholderText = L("clockairapp.timer.minutes", "Minutes");
|
||||
|
||||
SettingsHeaderTextBlock.Text = L("clockairapp.settings.title", "Clock settings");
|
||||
TimeFormatLabelTextBlock.Text = L("clockairapp.settings.time_format", "Time format");
|
||||
StartupTabLabelTextBlock.Text = L("clockairapp.settings.startup_tab", "Startup page");
|
||||
ShowSecondsCheckBox.Content = L("clockairapp.settings.show_seconds", "Show seconds");
|
||||
ActivateOnTimerFinishedCheckBox.Content = L("clockairapp.settings.activate_timer", "Activate window when timer finishes");
|
||||
}
|
||||
|
||||
private void PopulateSettingsControls()
|
||||
{
|
||||
_suppressSettingsEvents = true;
|
||||
try
|
||||
{
|
||||
SetComboItems(
|
||||
TimeFormatComboBox,
|
||||
[
|
||||
(ClockAirAppTimeFormatMode.System, L("clockairapp.settings.time_format.system", "Follow system")),
|
||||
(ClockAirAppTimeFormatMode.TwentyFourHour, L("clockairapp.settings.time_format.24h", "24-hour")),
|
||||
(ClockAirAppTimeFormatMode.TwelveHour, L("clockairapp.settings.time_format.12h", "12-hour"))
|
||||
],
|
||||
_settings.TimeFormatMode);
|
||||
SetComboItems(
|
||||
StartupTabComboBox,
|
||||
[
|
||||
(ClockAirAppTabIds.Last, L("clockairapp.settings.startup.last", "Last used")),
|
||||
(ClockAirAppTabIds.WorldClock, L("clockairapp.tab.world", "World")),
|
||||
(ClockAirAppTabIds.Stopwatch, L("clockairapp.tab.stopwatch", "Stopwatch")),
|
||||
(ClockAirAppTabIds.Timer, L("clockairapp.tab.timer", "Timer"))
|
||||
],
|
||||
_settings.StartupTab);
|
||||
ShowSecondsCheckBox.IsChecked = _settings.ShowSeconds;
|
||||
ActivateOnTimerFinishedCheckBox.IsChecked = _settings.ActivateOnTimerFinished;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressSettingsEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetComboItems(ComboBox comboBox, IEnumerable<(string Id, string Text)> items, string selectedId)
|
||||
{
|
||||
comboBox.Items.Clear();
|
||||
foreach (var item in items)
|
||||
{
|
||||
comboBox.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Tag = item.Id,
|
||||
Content = item.Text
|
||||
});
|
||||
}
|
||||
|
||||
comboBox.SelectedItem = comboBox.Items
|
||||
.OfType<ComboBoxItem>()
|
||||
.FirstOrDefault(item => string.Equals(item.Tag as string, selectedId, StringComparison.OrdinalIgnoreCase))
|
||||
?? comboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
}
|
||||
|
||||
private void SelectStartupTab()
|
||||
{
|
||||
var startupTab = ClockAirAppTabIds.Normalize(_settings.StartupTab, ClockAirAppTabIds.Last);
|
||||
var tab = string.Equals(startupTab, ClockAirAppTabIds.Last, StringComparison.OrdinalIgnoreCase)
|
||||
? ClockAirAppTabIds.Normalize(_settings.LastSelectedTab)
|
||||
: ClockAirAppTabIds.Normalize(startupTab);
|
||||
SelectTab(tab, save: false);
|
||||
}
|
||||
|
||||
private void OnClockTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
UpdateAll();
|
||||
}
|
||||
|
||||
private void UpdateAll()
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
UpdateWorldClock(now);
|
||||
UpdateStopwatch(now);
|
||||
UpdateTimer(now);
|
||||
}
|
||||
|
||||
private void UpdateWorldClock(DateTimeOffset now)
|
||||
{
|
||||
var localNow = now.LocalDateTime;
|
||||
LocalTimeTextBlock.Text = ClockAirAppTimeFormatter.FormatTime(localNow, _settings, _culture);
|
||||
LocalDateTextBlock.Text = localNow.ToString("yyyy-MM-dd dddd", _culture);
|
||||
LocalTimeZoneTextBlock.Text = TimeZoneInfo.Local.DisplayName;
|
||||
WorldSummaryTextBlock.Text = Lf("clockairapp.world.count", "{0} cities", _settings.WorldClockTimeZoneIds.Count);
|
||||
|
||||
var utcNow = now.UtcDateTime;
|
||||
foreach (var row in _worldClockRows)
|
||||
{
|
||||
var zonedTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, row.TimeZone);
|
||||
row.TimeTextBlock.Text = ClockAirAppTimeFormatter.FormatTime(zonedTime, _settings, _culture);
|
||||
row.DateTextBlock.Text = $"{ResolveRelativeDayLabel((zonedTime.Date - localNow.Date).Days)} - {zonedTime.ToString("yyyy-MM-dd", _culture)}";
|
||||
row.OffsetTextBlock.Text = ClockAirAppTimeFormatter.FormatUtcOffset(row.TimeZone.GetUtcOffset(utcNow));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStopwatch(DateTimeOffset now)
|
||||
{
|
||||
StopwatchElapsedTextBlock.Text = ClockAirAppTimeFormatter.FormatDuration(_stopwatchState.GetElapsed(now), includeMilliseconds: true);
|
||||
StopwatchStartPauseButton.Content = _stopwatchState.IsRunning
|
||||
? L("clockairapp.action.pause", "Pause")
|
||||
: L("clockairapp.action.start", "Start");
|
||||
StopwatchLapButton.IsEnabled = _stopwatchState.GetElapsed(now) > TimeSpan.Zero;
|
||||
StopwatchResetButton.IsEnabled = _stopwatchState.GetElapsed(now) > TimeSpan.Zero || _stopwatchState.Laps.Count > 0;
|
||||
}
|
||||
|
||||
private void UpdateTimer(DateTimeOffset now)
|
||||
{
|
||||
if (_timerState.Update(now))
|
||||
{
|
||||
TimerStatusTextBlock.Text = L("clockairapp.timer.finished", "Timer finished");
|
||||
if (_settings.ActivateOnTimerFinished && VisualRoot is Window window)
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
TimerRemainingTextBlock.Text = ClockAirAppTimeFormatter.FormatDuration(_timerState.GetRemaining(now));
|
||||
TimerStartPauseButton.Content = _timerState.IsRunning
|
||||
? L("clockairapp.action.pause", "Pause")
|
||||
: L("clockairapp.action.start", "Start");
|
||||
TimerResetButton.IsEnabled = _timerState.GetRemaining(now) < _timerState.Duration || _timerState.IsCompleted;
|
||||
if (!_timerState.IsCompleted && string.IsNullOrWhiteSpace(TimerStatusTextBlock.Text))
|
||||
{
|
||||
TimerStatusTextBlock.Text = Lf("clockairapp.timer.duration_status", "Duration {0}", ClockAirAppTimeFormatter.FormatDuration(_timerState.Duration));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTabButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button && button.Tag is string tab)
|
||||
{
|
||||
SelectTab(tab, save: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectTab(string tab, bool save)
|
||||
{
|
||||
_selectedTab = ClockAirAppTabIds.Normalize(tab);
|
||||
WorldPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.WorldClock, StringComparison.OrdinalIgnoreCase);
|
||||
StopwatchPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.Stopwatch, StringComparison.OrdinalIgnoreCase);
|
||||
TimerPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.Timer, StringComparison.OrdinalIgnoreCase);
|
||||
SettingsPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.Settings, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
WorldTabButton.IsChecked = WorldPage.IsVisible;
|
||||
StopwatchTabButton.IsChecked = StopwatchPage.IsVisible;
|
||||
TimerTabButton.IsChecked = TimerPage.IsVisible;
|
||||
SettingsTabButton.IsChecked = SettingsPage.IsVisible;
|
||||
|
||||
if (save)
|
||||
{
|
||||
_settings.LastSelectedTab = _selectedTab;
|
||||
_settingsStore.Save(_settings);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimeZoneSearchChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
PopulateTimeZoneCombo();
|
||||
}
|
||||
|
||||
private void PopulateTimeZoneCombo()
|
||||
{
|
||||
var query = TimeZoneSearchTextBox.Text?.Trim() ?? string.Empty;
|
||||
var zones = _allTimeZones
|
||||
.Where(zone => MatchesTimeZoneQuery(zone, query))
|
||||
.Take(80)
|
||||
.ToList();
|
||||
|
||||
TimeZoneComboBox.Items.Clear();
|
||||
foreach (var zone in zones)
|
||||
{
|
||||
TimeZoneComboBox.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Tag = zone.Id,
|
||||
Content = FormatTimeZoneOption(zone)
|
||||
});
|
||||
}
|
||||
|
||||
TimeZoneComboBox.SelectedItem = TimeZoneComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
}
|
||||
|
||||
private bool MatchesTimeZoneQuery(TimeZoneInfo zone, string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var cityName = ClockAirAppTimeFormatter.ResolveCityName(zone, _languageCode);
|
||||
return zone.Id.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
zone.DisplayName.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
zone.StandardName.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
cityName.Contains(query, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string FormatTimeZoneOption(TimeZoneInfo zone)
|
||||
{
|
||||
return $"{ClockAirAppTimeFormatter.FormatUtcOffset(zone.GetUtcOffset(DateTime.UtcNow))} | {ClockAirAppTimeFormatter.ResolveCityName(zone, _languageCode)} | {zone.StandardName}";
|
||||
}
|
||||
|
||||
private void OnAddCityClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (TimeZoneComboBox.SelectedItem is not ComboBoxItem item || item.Tag is not string zoneId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings.WorldClockTimeZoneIds.Any(existing => string.Equals(existing, zoneId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.WorldClockTimeZoneIds.Add(zoneId);
|
||||
SaveWorldClockSettings();
|
||||
}
|
||||
|
||||
private void RebuildWorldClockRows()
|
||||
{
|
||||
_worldClockRows.Clear();
|
||||
WorldClockRowsPanel.Children.Clear();
|
||||
for (var index = 0; index < _settings.WorldClockTimeZoneIds.Count; index++)
|
||||
{
|
||||
var timeZone = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(_settings.WorldClockTimeZoneIds[index]);
|
||||
AddWorldClockRow(timeZone, index);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddWorldClockRow(TimeZoneInfo timeZone, int index)
|
||||
{
|
||||
var cityText = new TextBlock
|
||||
{
|
||||
Text = ClockAirAppTimeFormatter.ResolveCityName(timeZone, _languageCode),
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = TryGetBrush("AirAppTitleTextBrush", "#FF171A20")
|
||||
};
|
||||
var timeText = new TextBlock
|
||||
{
|
||||
FontSize = 24,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
LetterSpacing = 0,
|
||||
Foreground = TryGetBrush("AirAppTitleTextBrush", "#FF171A20"),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
var dateText = new TextBlock
|
||||
{
|
||||
FontSize = 12,
|
||||
Foreground = TryGetBrush("AirAppSecondaryTextBrush", "#FF657080")
|
||||
};
|
||||
var offsetText = new TextBlock
|
||||
{
|
||||
FontSize = 12,
|
||||
Foreground = TryGetBrush("AirAppSecondaryTextBrush", "#FF657080"),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
|
||||
var upButton = CreateIconButton("↑", L("clockairapp.action.move_up", "Move up"));
|
||||
upButton.IsEnabled = index > 0;
|
||||
upButton.Click += (_, _) => MoveWorldClock(index, -1);
|
||||
|
||||
var downButton = CreateIconButton("↓", L("clockairapp.action.move_down", "Move down"));
|
||||
downButton.IsEnabled = index < _settings.WorldClockTimeZoneIds.Count - 1;
|
||||
downButton.Click += (_, _) => MoveWorldClock(index, 1);
|
||||
|
||||
var removeButton = CreateIconButton("×", L("clockairapp.action.remove", "Remove"));
|
||||
removeButton.IsEnabled = _settings.WorldClockTimeZoneIds.Count > 1;
|
||||
removeButton.Click += (_, _) => RemoveWorldClock(index);
|
||||
|
||||
var row = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
|
||||
ColumnSpacing = 8
|
||||
};
|
||||
var leftStack = new StackPanel
|
||||
{
|
||||
Spacing = 3,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
cityText,
|
||||
dateText
|
||||
}
|
||||
};
|
||||
var timeStack = new StackPanel
|
||||
{
|
||||
Spacing = 2,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
timeText,
|
||||
offsetText
|
||||
}
|
||||
};
|
||||
row.Children.Add(leftStack);
|
||||
row.Children.Add(timeStack);
|
||||
row.Children.Add(upButton);
|
||||
row.Children.Add(downButton);
|
||||
row.Children.Add(removeButton);
|
||||
Grid.SetColumn(timeStack, 1);
|
||||
Grid.SetColumn(upButton, 2);
|
||||
Grid.SetColumn(downButton, 3);
|
||||
Grid.SetColumn(removeButton, 4);
|
||||
|
||||
WorldClockRowsPanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#0A000000")),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(12, 10),
|
||||
Child = row
|
||||
});
|
||||
|
||||
_worldClockRows.Add(new WorldClockRowVisual
|
||||
{
|
||||
TimeZone = timeZone,
|
||||
TimeTextBlock = timeText,
|
||||
DateTextBlock = dateText,
|
||||
OffsetTextBlock = offsetText
|
||||
});
|
||||
}
|
||||
|
||||
private Button CreateIconButton(string text, string tooltip)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Content = text,
|
||||
Classes = { "clock-icon-command" }
|
||||
};
|
||||
ToolTip.SetTip(button, tooltip);
|
||||
return button;
|
||||
}
|
||||
|
||||
private void MoveWorldClock(int index, int delta)
|
||||
{
|
||||
var nextIndex = index + delta;
|
||||
if (index < 0 || nextIndex < 0 || index >= _settings.WorldClockTimeZoneIds.Count || nextIndex >= _settings.WorldClockTimeZoneIds.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
(_settings.WorldClockTimeZoneIds[index], _settings.WorldClockTimeZoneIds[nextIndex]) =
|
||||
(_settings.WorldClockTimeZoneIds[nextIndex], _settings.WorldClockTimeZoneIds[index]);
|
||||
SaveWorldClockSettings();
|
||||
}
|
||||
|
||||
private void RemoveWorldClock(int index)
|
||||
{
|
||||
if (_settings.WorldClockTimeZoneIds.Count <= 1 || index < 0 || index >= _settings.WorldClockTimeZoneIds.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.WorldClockTimeZoneIds.RemoveAt(index);
|
||||
SaveWorldClockSettings();
|
||||
}
|
||||
|
||||
private void SaveWorldClockSettings()
|
||||
{
|
||||
_settings = ClockAirAppSettingsSnapshot.Normalize(_settings);
|
||||
_settingsStore.Save(_settings);
|
||||
RebuildWorldClockRows();
|
||||
UpdateWorldClock(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void OnStopwatchStartPauseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
var now = DateTimeOffset.Now;
|
||||
if (_stopwatchState.IsRunning)
|
||||
{
|
||||
_stopwatchState.Pause(now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stopwatchState.StartOrResume(now);
|
||||
}
|
||||
|
||||
UpdateStopwatch(now);
|
||||
}
|
||||
|
||||
private void OnStopwatchLapClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_ = _stopwatchState.AddLap(DateTimeOffset.Now);
|
||||
RebuildStopwatchLaps();
|
||||
}
|
||||
|
||||
private void OnStopwatchResetClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_stopwatchState.Reset();
|
||||
RebuildStopwatchLaps();
|
||||
UpdateStopwatch(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void RebuildStopwatchLaps()
|
||||
{
|
||||
StopwatchLapsPanel.Children.Clear();
|
||||
for (var index = 0; index < _stopwatchState.Laps.Count; index++)
|
||||
{
|
||||
var lap = _stopwatchState.Laps[index];
|
||||
StopwatchLapsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Lf("clockairapp.stopwatch.lap_format", "Lap {0} {1}", _stopwatchState.Laps.Count - index, ClockAirAppTimeFormatter.FormatDuration(lap, includeMilliseconds: true)),
|
||||
Foreground = TryGetBrush("AirAppSecondaryTextBrush", "#FF657080"),
|
||||
FontSize = 13
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimerPresetClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button &&
|
||||
button.Tag is string minutesText &&
|
||||
int.TryParse(minutesText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var minutes))
|
||||
{
|
||||
SetTimerDuration(minutes);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimerApplyClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (!int.TryParse(TimerMinutesTextBox.Text, NumberStyles.Integer, CultureInfo.CurrentCulture, out var minutes))
|
||||
{
|
||||
TimerStatusTextBlock.Text = L("clockairapp.timer.invalid", "Enter a valid minute value.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetTimerDuration(minutes);
|
||||
}
|
||||
|
||||
private void SetTimerDuration(int minutes)
|
||||
{
|
||||
minutes = Math.Clamp(minutes, 1, 24 * 60);
|
||||
TimerMinutesTextBox.Text = minutes.ToString(CultureInfo.CurrentCulture);
|
||||
_timerState.SetDuration(TimeSpan.FromMinutes(minutes));
|
||||
TimerStatusTextBlock.Text = Lf("clockairapp.timer.duration_status", "Duration {0}", ClockAirAppTimeFormatter.FormatDuration(_timerState.Duration));
|
||||
UpdateTimer(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void OnTimerStartPauseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
var now = DateTimeOffset.Now;
|
||||
if (_timerState.IsRunning)
|
||||
{
|
||||
_timerState.Pause(now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_timerState.StartOrResume(now);
|
||||
TimerStatusTextBlock.Text = string.Empty;
|
||||
}
|
||||
|
||||
UpdateTimer(now);
|
||||
}
|
||||
|
||||
private void OnTimerResetClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_timerState.Reset();
|
||||
TimerStatusTextBlock.Text = Lf("clockairapp.timer.duration_status", "Duration {0}", ClockAirAppTimeFormatter.FormatDuration(_timerState.Duration));
|
||||
UpdateTimer(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
SaveSettingsFromControls(sender);
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
SaveSettingsFromControls(sender);
|
||||
}
|
||||
|
||||
private void SaveSettingsFromControls(object? sender)
|
||||
{
|
||||
_ = sender;
|
||||
if (_suppressSettingsEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.TimeFormatMode = TimeFormatComboBox.SelectedItem is ComboBoxItem timeFormatItem && timeFormatItem.Tag is string timeFormat
|
||||
? timeFormat
|
||||
: ClockAirAppTimeFormatMode.System;
|
||||
_settings.StartupTab = StartupTabComboBox.SelectedItem is ComboBoxItem startupItem && startupItem.Tag is string startupTab
|
||||
? startupTab
|
||||
: ClockAirAppTabIds.Last;
|
||||
_settings.ShowSeconds = ShowSecondsCheckBox.IsChecked == true;
|
||||
_settings.ActivateOnTimerFinished = ActivateOnTimerFinishedCheckBox.IsChecked == true;
|
||||
_settingsStore.Save(_settings);
|
||||
UpdateAll();
|
||||
}
|
||||
|
||||
private string ResolveRelativeDayLabel(int dayDelta)
|
||||
{
|
||||
if (dayDelta < 0)
|
||||
{
|
||||
return L("worldclock.widget.yesterday", "Yesterday");
|
||||
}
|
||||
|
||||
if (dayDelta > 0)
|
||||
{
|
||||
return L("worldclock.widget.tomorrow", "Tomorrow");
|
||||
}
|
||||
|
||||
return L("worldclock.widget.today", "Today");
|
||||
}
|
||||
|
||||
private IBrush TryGetBrush(string resourceKey, string fallbackColor)
|
||||
{
|
||||
return this.TryFindResource(resourceKey, out var value) && value is IBrush brush
|
||||
? brush
|
||||
: new SolidColorBrush(Color.Parse(fallbackColor));
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private string Lf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return string.Format(_culture, L(key, fallback), args);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\LanMountainDesktop\Assets\Fonts\**" Link="Assets\Fonts\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
<AvaloniaResource Include="..\LanMountainDesktop\Assets\logo_nightly.png" Link="Assets\logo_nightly.png" />
|
||||
<None Include="..\LanMountainDesktop\Localization\*.json"
|
||||
Link="Localization\%(Filename)%(Extension)"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user