mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
636 lines
21 KiB
C#
636 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Media;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using LanMountainDesktop.Models;
|
|
using LanMountainDesktop.Services;
|
|
|
|
namespace LanMountainDesktop.Views.Components;
|
|
|
|
public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
|
|
{
|
|
private const int WaveBarCount = 22;
|
|
|
|
private readonly DispatcherTimer _uiTimer = new()
|
|
{
|
|
Interval = TimeSpan.FromMilliseconds(96)
|
|
};
|
|
|
|
private readonly IAudioRecorderService _audioRecorderService = AudioRecorderServiceFactory.CreateRecorder();
|
|
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
|
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
|
private readonly LocalizationService _localizationService = new();
|
|
private readonly List<Border> _waveBars = [];
|
|
private readonly double[] _waveLevels = new double[WaveBarCount];
|
|
|
|
private string _languageCode = "zh-CN";
|
|
private string _lastSavedFilePath = string.Empty;
|
|
private double _currentCellSize = 48;
|
|
private bool _isAttached;
|
|
private bool _isOnActivePage = true;
|
|
private bool _pausedStudyMonitoringForRecording;
|
|
private bool _isNightVisual = true;
|
|
private bool _isDisposed;
|
|
|
|
public RecordingWidget()
|
|
{
|
|
InitializeComponent();
|
|
|
|
_uiTimer.Tick += OnUiTick;
|
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
|
SizeChanged += OnSizeChanged;
|
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
|
|
|
InitializeWaveBars();
|
|
ReloadLanguageCode();
|
|
ApplyCellSize(_currentCellSize);
|
|
RefreshVisual();
|
|
}
|
|
|
|
public void ApplyCellSize(double cellSize)
|
|
{
|
|
_currentCellSize = Math.Max(1, cellSize);
|
|
var rawScale = ResolveScale();
|
|
var chromeScale = Math.Clamp(rawScale, 0.62, 2.0);
|
|
var contentScale = Math.Clamp(rawScale, 0.74, 1.0);
|
|
|
|
var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56);
|
|
RootBorder.CornerRadius = rootRadius;
|
|
RootBorder.Padding = new Thickness(0);
|
|
RecorderCardBorder.CornerRadius = rootRadius;
|
|
RecorderContentGrid.Margin = new Thickness(
|
|
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
|
ComponentChromeCornerRadiusHelper.SafeValue(18 * contentScale, 10, 22),
|
|
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
|
ComponentChromeCornerRadiusHelper.SafeValue(18 * contentScale, 10, 24));
|
|
|
|
var sideButtonSize = Math.Clamp(54 * contentScale, 34, 58);
|
|
DiscardButtonBorder.Width = sideButtonSize;
|
|
DiscardButtonBorder.Height = sideButtonSize;
|
|
DiscardButtonBorder.CornerRadius = new CornerRadius(sideButtonSize / 2d);
|
|
DiscardIcon.FontSize = Math.Clamp(20 * contentScale, 14, 22);
|
|
|
|
SaveButtonBorder.Width = sideButtonSize;
|
|
SaveButtonBorder.Height = sideButtonSize;
|
|
SaveButtonBorder.CornerRadius = new CornerRadius(sideButtonSize / 2d);
|
|
SaveIcon.FontSize = Math.Clamp(22 * contentScale, 15, 24);
|
|
|
|
var centerButtonSize = Math.Clamp(68 * contentScale, 42, 72);
|
|
RecordToggleButtonBorder.Width = centerButtonSize;
|
|
RecordToggleButtonBorder.Height = centerButtonSize;
|
|
RecordToggleButtonBorder.CornerRadius = new CornerRadius(centerButtonSize / 2d);
|
|
var centerIconSize = Math.Clamp(20 * contentScale, 14, 24);
|
|
PauseGlyphIcon.FontSize = centerIconSize;
|
|
PlayGlyphIcon.FontSize = centerIconSize;
|
|
var recordDotSize = Math.Clamp(15 * contentScale, 10, 16);
|
|
RecordDot.Width = recordDotSize;
|
|
RecordDot.Height = recordDotSize;
|
|
|
|
WaveformRowGrid.Margin = new Thickness(0, Math.Clamp(12 * contentScale, 6, 16), 0, 0);
|
|
CenterNeedle.Height = Math.Clamp(32 * contentScale, 18, 34);
|
|
FutureLine.Margin = new Thickness(Math.Clamp(4 * contentScale, 2, 6), 0, 0, 0);
|
|
FutureLine.Height = Math.Clamp(2 * contentScale, 1, 3);
|
|
ControlButtonsGrid.Margin = new Thickness(0, Math.Clamp(16 * contentScale, 8, 20), 0, 0);
|
|
ControlButtonsGrid.ColumnSpacing = Math.Clamp(16 * contentScale, 8, 16);
|
|
HintTextBlock.Margin = new Thickness(0, Math.Clamp(8 * contentScale, 4, 10), 0, 0);
|
|
|
|
WaveformBarsPanel.Spacing = Math.Clamp(3 * contentScale, 1.6, 3.4);
|
|
TitleTextBlock.FontSize = Math.Clamp(19 * contentScale, 12, 20);
|
|
TimerTextBlock.FontSize = Math.Clamp(66 * contentScale, 34, 66);
|
|
HintTextBlock.FontSize = Math.Clamp(13 * contentScale, 9, 13);
|
|
|
|
UpdateWaveformVisual();
|
|
}
|
|
|
|
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
|
{
|
|
_ = isEditMode;
|
|
var wasOnActivePage = _isOnActivePage;
|
|
_isOnActivePage = isOnActivePage;
|
|
UpdateUiTimerState();
|
|
|
|
if (!wasOnActivePage && _isOnActivePage && _isAttached)
|
|
{
|
|
ReloadLanguageCode();
|
|
RefreshVisual();
|
|
}
|
|
}
|
|
|
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
|
{
|
|
_isAttached = true;
|
|
UpdateUiTimerState();
|
|
ReloadLanguageCode();
|
|
RefreshVisual();
|
|
}
|
|
|
|
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
|
{
|
|
_isAttached = false;
|
|
UpdateUiTimerState();
|
|
|
|
var snapshot = _audioRecorderService.GetSnapshot();
|
|
if (snapshot.State is not AudioRecorderRuntimeState.Recording and not AudioRecorderRuntimeState.Paused)
|
|
{
|
|
ResumeStudyMonitoringIfNeeded();
|
|
}
|
|
}
|
|
|
|
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
|
{
|
|
ApplyCellSize(_currentCellSize);
|
|
}
|
|
|
|
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
|
{
|
|
_isNightVisual = ResolveNightMode();
|
|
ApplyNightModeVisual();
|
|
}
|
|
|
|
private bool ResolveNightMode()
|
|
{
|
|
if (ActualThemeVariant == ThemeVariant.Dark)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ActualThemeVariant == ThemeVariant.Light)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
|
value is ISolidColorBrush brush)
|
|
{
|
|
return CalculateRelativeLuminance(brush.Color) < 0.45;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static double CalculateRelativeLuminance(Color color)
|
|
{
|
|
static double ToLinear(double channel)
|
|
{
|
|
return channel <= 0.03928
|
|
? channel / 12.92
|
|
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
var r = ToLinear(color.R / 255d);
|
|
var g = ToLinear(color.G / 255d);
|
|
var b = ToLinear(color.B / 255d);
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
}
|
|
|
|
private void ApplyNightModeVisual()
|
|
{
|
|
RootBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#ECEFF3"));
|
|
RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#D9DEE7"));
|
|
|
|
TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#11151D"));
|
|
TimerTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#A4A9B2"));
|
|
FutureLine.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#A3A8B3"));
|
|
|
|
DiscardButtonBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F8FAFD"));
|
|
DiscardButtonBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4451") : Color.Parse("#E0E5EC"));
|
|
DiscardIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#141922"));
|
|
|
|
SaveButtonBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F8FAFD"));
|
|
SaveButtonBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4451") : Color.Parse("#E0E5EC"));
|
|
SaveIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#141922"));
|
|
|
|
HintTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#7A818E"));
|
|
}
|
|
|
|
private void OnUiTick(object? sender, EventArgs e)
|
|
{
|
|
if (!_isAttached || !_isOnActivePage)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RefreshVisual();
|
|
}
|
|
|
|
private void UpdateUiTimerState()
|
|
{
|
|
if (_isAttached && _isOnActivePage)
|
|
{
|
|
if (!_uiTimer.IsEnabled)
|
|
{
|
|
_uiTimer.Start();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_uiTimer.Stop();
|
|
}
|
|
|
|
private void OnDiscardButtonPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_audioRecorderService.Discard();
|
|
ResumeStudyMonitoringIfNeeded();
|
|
RefreshVisual();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnRecordToggleButtonPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var snapshot = _audioRecorderService.GetSnapshot();
|
|
if (!snapshot.IsSupported)
|
|
{
|
|
RefreshVisual();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (snapshot.State == AudioRecorderRuntimeState.Recording)
|
|
{
|
|
_audioRecorderService.Pause();
|
|
}
|
|
else
|
|
{
|
|
_ = TryStartRecordingWithMonitoringHandoff();
|
|
}
|
|
|
|
RefreshVisual();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnSaveButtonPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var snapshot = _audioRecorderService.GetSnapshot();
|
|
if (!snapshot.IsSupported)
|
|
{
|
|
RefreshVisual();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (snapshot.State == AudioRecorderRuntimeState.Recording)
|
|
{
|
|
_audioRecorderService.Pause();
|
|
}
|
|
|
|
var (wasCancelled, outputPath) = await PickSavePathAsync();
|
|
if (wasCancelled)
|
|
{
|
|
RefreshVisual();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
_ = _audioRecorderService.StopAndSave(outputPath);
|
|
ResumeStudyMonitoringIfNeeded();
|
|
RefreshVisual();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void RefreshVisual()
|
|
{
|
|
var snapshot = _audioRecorderService.GetSnapshot();
|
|
if (_pausedStudyMonitoringForRecording &&
|
|
snapshot.State is AudioRecorderRuntimeState.Ready or AudioRecorderRuntimeState.Error or AudioRecorderRuntimeState.Unsupported)
|
|
{
|
|
ResumeStudyMonitoringIfNeeded();
|
|
snapshot = _audioRecorderService.GetSnapshot();
|
|
}
|
|
|
|
TitleTextBlock.Text = L("recording.widget.title", "Recorder");
|
|
TimerTextBlock.Text = FormatDuration(snapshot.Duration);
|
|
|
|
if (snapshot.State == AudioRecorderRuntimeState.Recording)
|
|
{
|
|
PushWaveLevel(snapshot.InputLevel);
|
|
}
|
|
else if (snapshot.State != AudioRecorderRuntimeState.Paused)
|
|
{
|
|
ClearWaveLevels();
|
|
}
|
|
|
|
UpdateWaveformVisual();
|
|
|
|
ApplyControlState(snapshot);
|
|
}
|
|
|
|
private void ApplyControlState(AudioRecorderSnapshot snapshot)
|
|
{
|
|
var isSupported = snapshot.IsSupported;
|
|
var canFinalize = snapshot.State == AudioRecorderRuntimeState.Recording ||
|
|
snapshot.State == AudioRecorderRuntimeState.Paused;
|
|
var isReady = snapshot.State == AudioRecorderRuntimeState.Ready;
|
|
|
|
TitleTextBlock.IsVisible = false;
|
|
DiscardButtonBorder.IsVisible = canFinalize;
|
|
SaveButtonBorder.IsVisible = canFinalize;
|
|
DiscardButtonBorder.IsHitTestVisible = isSupported && canFinalize;
|
|
SaveButtonBorder.IsHitTestVisible = isSupported && canFinalize;
|
|
RecordToggleButtonBorder.IsHitTestVisible = isSupported;
|
|
|
|
DiscardButtonBorder.Opacity = DiscardButtonBorder.IsHitTestVisible ? 1 : 0.42;
|
|
SaveButtonBorder.Opacity = SaveButtonBorder.IsHitTestVisible ? 1 : 0.42;
|
|
RecordToggleButtonBorder.Opacity = RecordToggleButtonBorder.IsHitTestVisible ? 1 : 0.54;
|
|
|
|
if (!isSupported)
|
|
{
|
|
TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#A8B1C2" : "#B2B7C0");
|
|
}
|
|
else if (isReady)
|
|
{
|
|
TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#A8B1C2" : "#A4A9B2");
|
|
}
|
|
else
|
|
{
|
|
TimerTextBlock.Foreground = CreateBrush(_isNightVisual ? "#E8EAED" : "#151922");
|
|
}
|
|
HintTextBlock.IsVisible = !isReady || !isSupported;
|
|
|
|
RecordDot.IsVisible = snapshot.State == AudioRecorderRuntimeState.Ready;
|
|
PauseGlyphIcon.IsVisible = snapshot.State == AudioRecorderRuntimeState.Recording;
|
|
PlayGlyphIcon.IsVisible = snapshot.State == AudioRecorderRuntimeState.Paused;
|
|
|
|
if (!isSupported)
|
|
{
|
|
HintTextBlock.Text = L("recording.widget.hint.unsupported", "Microphone is unavailable");
|
|
return;
|
|
}
|
|
|
|
if (snapshot.State == AudioRecorderRuntimeState.Recording)
|
|
{
|
|
HintTextBlock.Text = L("recording.widget.hint.recording", "Recording");
|
|
return;
|
|
}
|
|
|
|
if (snapshot.State == AudioRecorderRuntimeState.Paused)
|
|
{
|
|
HintTextBlock.Text = L("recording.widget.hint.paused", "Paused");
|
|
return;
|
|
}
|
|
|
|
if (snapshot.State == AudioRecorderRuntimeState.Error)
|
|
{
|
|
HintTextBlock.Text = string.IsNullOrWhiteSpace(snapshot.LastError)
|
|
? L("recording.widget.hint.error", "Recording failed")
|
|
: snapshot.LastError;
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(snapshot.LastSavedFilePath) &&
|
|
!string.Equals(snapshot.LastSavedFilePath, _lastSavedFilePath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_lastSavedFilePath = snapshot.LastSavedFilePath;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(_lastSavedFilePath))
|
|
{
|
|
var fileName = Path.GetFileName(_lastSavedFilePath);
|
|
HintTextBlock.Text = string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
L("recording.widget.hint.saved_format", "Saved {0}"),
|
|
fileName);
|
|
return;
|
|
}
|
|
|
|
HintTextBlock.Text = L("recording.widget.hint.ready", "Tap red button to record");
|
|
}
|
|
|
|
private bool TryStartRecordingWithMonitoringHandoff()
|
|
{
|
|
if (_audioRecorderService.StartOrResume())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryPauseStudyMonitoringForRecording())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_audioRecorderService.StartOrResume())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
ResumeStudyMonitoringIfNeeded();
|
|
return false;
|
|
}
|
|
|
|
private bool TryPauseStudyMonitoringForRecording()
|
|
{
|
|
if (_pausedStudyMonitoringForRecording)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var snapshot = _studyAnalyticsService.GetSnapshot();
|
|
if (snapshot.State != StudyAnalyticsRuntimeState.Running)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_studyAnalyticsService.PauseMonitoring())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_pausedStudyMonitoringForRecording = true;
|
|
return true;
|
|
}
|
|
|
|
private void ResumeStudyMonitoringIfNeeded()
|
|
{
|
|
if (!_pausedStudyMonitoringForRecording)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_pausedStudyMonitoringForRecording = false;
|
|
_ = _studyAnalyticsService.StartOrResumeMonitoring();
|
|
}
|
|
|
|
private void InitializeWaveBars()
|
|
{
|
|
if (_waveBars.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < WaveBarCount; i++)
|
|
{
|
|
var bar = new Border
|
|
{
|
|
Width = 3,
|
|
Height = 6,
|
|
CornerRadius = new CornerRadius(1.5),
|
|
Background = CreateBrush("#121722"),
|
|
Opacity = 0.24,
|
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
|
};
|
|
|
|
_waveBars.Add(bar);
|
|
WaveformBarsPanel.Children.Add(bar);
|
|
}
|
|
}
|
|
|
|
private void PushWaveLevel(double level)
|
|
{
|
|
for (var i = 0; i < _waveLevels.Length - 1; i++)
|
|
{
|
|
_waveLevels[i] = _waveLevels[i + 1];
|
|
}
|
|
|
|
var previous = _waveLevels[^2];
|
|
var target = Math.Clamp(level, 0, 1);
|
|
_waveLevels[^1] = Math.Clamp((previous * 0.35) + (target * 0.65), 0, 1);
|
|
}
|
|
|
|
private void ClearWaveLevels()
|
|
{
|
|
Array.Fill(_waveLevels, 0);
|
|
}
|
|
|
|
private void UpdateWaveformVisual()
|
|
{
|
|
var scale = Math.Clamp(ResolveScale(), 0.74, 1.0);
|
|
var barWidth = Math.Clamp(3 * scale, 1.8, 3.2);
|
|
for (var i = 0; i < _waveBars.Count; i++)
|
|
{
|
|
var bar = _waveBars[i];
|
|
var eased = Math.Pow(Math.Clamp(_waveLevels[i], 0, 1), 0.62);
|
|
bar.Width = barWidth;
|
|
bar.Height = Math.Clamp((4 + (eased * 24)) * scale, 3, 30);
|
|
bar.CornerRadius = new CornerRadius(Math.Clamp(barWidth / 2d, 1, 2));
|
|
bar.Opacity = Math.Clamp(0.20 + (eased * 0.82), 0.20, 1.0);
|
|
}
|
|
}
|
|
|
|
private void ReloadLanguageCode()
|
|
{
|
|
try
|
|
{
|
|
var snapshot = _settingsService.Load();
|
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
|
}
|
|
catch
|
|
{
|
|
_languageCode = "zh-CN";
|
|
}
|
|
}
|
|
|
|
private string L(string key, string fallback)
|
|
{
|
|
return _localizationService.GetString(_languageCode, key, fallback);
|
|
}
|
|
|
|
private double ResolveScale()
|
|
{
|
|
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 2.0);
|
|
var widthScale = Bounds.Width > 1
|
|
? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * 2), 0.60, 2.0)
|
|
: 1;
|
|
var heightScale = Bounds.Height > 1
|
|
? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * 2), 0.60, 2.0)
|
|
: 1;
|
|
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.02), 0.58, 2.04);
|
|
}
|
|
|
|
private static string FormatDuration(TimeSpan duration)
|
|
{
|
|
if (duration.TotalHours >= 1)
|
|
{
|
|
return duration.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
return duration.ToString(@"mm\:ss", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static IBrush CreateBrush(string colorHex)
|
|
{
|
|
return new SolidColorBrush(Color.Parse(colorHex));
|
|
}
|
|
|
|
private async Task<(bool WasCancelled, string? OutputPath)> PickSavePathAsync()
|
|
{
|
|
var suggestedName = $"recording_{DateTime.Now:yyyyMMdd_HHmmss}.wav";
|
|
var topLevel = TopLevel.GetTopLevel(this);
|
|
var storageProvider = topLevel?.StorageProvider;
|
|
if (storageProvider is null)
|
|
{
|
|
return (false, null);
|
|
}
|
|
|
|
var saveFile = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
{
|
|
Title = L("recording.widget.save_picker_title", "Save recording"),
|
|
SuggestedFileName = suggestedName,
|
|
DefaultExtension = "wav",
|
|
FileTypeChoices =
|
|
[
|
|
new FilePickerFileType(L("recording.widget.save_picker_type", "WAV audio"))
|
|
{
|
|
Patterns = ["*.wav"],
|
|
MimeTypes = ["audio/wav", "audio/x-wav"]
|
|
}
|
|
]
|
|
});
|
|
|
|
if (saveFile is null)
|
|
{
|
|
return (true, null);
|
|
}
|
|
|
|
var path = saveFile.Path;
|
|
if (path is null || !path.IsFile)
|
|
{
|
|
return (true, null);
|
|
}
|
|
|
|
return (false, path.LocalPath);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isDisposed = true;
|
|
|
|
_uiTimer.Stop();
|
|
_uiTimer.Tick -= OnUiTick;
|
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
|
SizeChanged -= OnSizeChanged;
|
|
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
|
|
|
_audioRecorderService.Dispose();
|
|
}
|
|
}
|