Files
BetterLyrics/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MusicGalleryViewModel.cs
Zhe Fang 45eb2a1506 Enhance application functionality to support single instance and multiple languages
Added single instance support in `App.xaml.cs` to ensure that only one instance of the application can run, and introduced `Mutex` and `EnsureSingleInstance()` methods. Updated multi-language support and added new string resources to serve users of different languages.

Refactored the play queue processing logic, using `PlayQueueItem` instead of `Track`, and introduced a new song tag information class in `SongsTabInfo.cs`. Updated UI components, replaced old image resources, and improved user experience.

In addition, removed the unused `BuildDate` property and simplified the logic of the settings page. Together, these changes improve the stability, usability, and maintainability of the application.
2025-07-27 23:27:04 -04:00

421 lines
16 KiB
C#

using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Media;
using Windows.Media.Core;
using Windows.Media.Playback;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class MusicGalleryViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalMediaFolder>>>
{
private readonly ILibWatcherService _libWatcherService;
private readonly MediaPlayer _mediaPlayer = new();
private readonly MediaTimelineController _timelineController = new();
private readonly SystemMediaTransportControls _smtc;
// All songs
private List<Track> _tracks = [];
// Songs in current playlist
private List<Track> _playlistTracks = [];
// Filtered songs based on search query for current playlist
private List<Track> _filteredTracks = [];
[ObservableProperty]
public partial bool IsLocalMediaNotFound { get; set; }
/// <summary>
/// Grouped tracks after filtering and sorting for current playlist
/// </summary>
[ObservableProperty]
public partial ObservableCollection<GroupInfoList> GroupedTracks { get; set; } = [];
[ObservableProperty]
public partial List<Track> SelectedTracks { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; } = [];
public PlayQueueItem? PlayingQueueItem => TrackPlayingQueue.ElementAtOrDefault(PlayingSongIndex);
[ObservableProperty]
public partial PlaybackOrder PlaybackOrder { get; set; }
[ObservableProperty]
public partial CommonSongProperty SongOrderType { get; set; } = CommonSongProperty.Title;
[ObservableProperty]
public partial ObservableCollection<SongsTabInfo> SongsTabInfoList { get; set; } = [];
[ObservableProperty]
public partial int SelectedSongsTabInfoIndex { get; set; } = 0;
public SongsTabInfo? SelectedSongsTabInfo => SongsTabInfoList.ElementAtOrDefault(SelectedSongsTabInfoIndex);
[ObservableProperty]
public partial bool IsDataLoading { get; set; } = false;
[ObservableProperty]
public partial Track TrackRightTapped { get; set; } = new();
[ObservableProperty]
public partial int PlayingSongIndex { get; set; } = -1;
[ObservableProperty]
public partial int DisplayedPlayingSongIndex { get; set; } = 0;
[ObservableProperty]
public partial string SongSearchQuery { get; set; } = string.Empty;
public MusicGalleryViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService) : base(settingsService)
{
SongsTabInfoList.Add(new SongsTabInfo(App.ResourceLoader!.GetString("MusicGalleryPageAllSongs"), "\uE8A9", false, CommonSongProperty.Title, string.Empty));
RefreshSongs();
PlaybackOrder = _settingsService.PlaybackOrder;
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
_timelineController = _mediaPlayer.TimelineController = new();
_timelineController.PositionChanged += TimelineController_PositionChanged;
_smtc = _mediaPlayer.SystemMediaTransportControls;
_mediaPlayer.CommandManager.IsEnabled = false;
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
_smtc.ButtonPressed += Smtc_ButtonPressed;
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
_libWatcherService = libWatcherService;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
}
private void MediaPlayer_MediaEnded(MediaPlayer sender, object args)
{
PlayNextTrack();
}
public void PlayNextTrack()
{
switch (PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (PlayingSongIndex < TrackPlayingQueue.Count - 1)
{
PlayingSongIndex++;
}
else
{
PlayingSongIndex = 0;
}
PlayTrack(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (TrackPlayingQueue.Count > 0)
{
PlayingSongIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
PlayTrack(PlayingQueueItem);
});
break;
default:
break;
}
}
private void PlayPreviousTrack()
{
switch (PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (PlayingSongIndex > 0)
{
PlayingSongIndex--;
}
else
{
PlayingSongIndex = TrackPlayingQueue.Count - 1;
}
PlayTrack(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (TrackPlayingQueue.Count > 0)
{
PlayingSongIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
PlayTrack(PlayingQueueItem);
});
break;
default:
break;
}
}
private void Smtc_PlaybackPositionChangeRequested(SystemMediaTransportControls sender, PlaybackPositionChangeRequestedEventArgs args)
{
_timelineController.Position = args.RequestedPlaybackPosition;
}
private void MediaPlayer_MediaOpened(MediaPlayer sender, object args)
{
_timelineController.Start();
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
}
private void TimelineController_PositionChanged(MediaTimelineController sender, object args)
{
_smtc.UpdateTimelineProperties(new SystemMediaTransportControlsTimelineProperties()
{
Position = sender.Position,
EndTime = _mediaPlayer.PlaybackSession.NaturalDuration
});
}
private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
switch (args.Button)
{
case SystemMediaTransportControlsButton.Play:
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
_timelineController.Resume();
break;
case SystemMediaTransportControlsButton.Pause:
_smtc.PlaybackStatus = MediaPlaybackStatus.Paused;
_timelineController.Pause();
break;
case SystemMediaTransportControlsButton.Next:
PlayNextTrack();
break;
case SystemMediaTransportControlsButton.Previous:
PlayPreviousTrack();
break;
}
}
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, Events.LibChangedEventArgs e)
{
RefreshSongs();
}
public void RefreshSongs()
{
_dispatcherQueueTimer.Debounce(() =>
{
IsDataLoading = true;
_tracks.Clear();
Task.Run(() =>
{
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
Track track = new(file);
if (track.Duration <= 0) continue;
_tracks.Add(track);
}
}
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
ApplyPlaylist();
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
IsDataLoading = false;
});
});
}, TimeSpan.FromMilliseconds(100));
}
public void ApplyPlaylist()
{
if (SelectedSongsTabInfo?.FilterValue == string.Empty)
{
_playlistTracks = _tracks;
}
else
{
switch (SelectedSongsTabInfo?.FilterProperty)
{
case CommonSongProperty.Title:
_playlistTracks = _tracks.Where(t => t.Title.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
case CommonSongProperty.Album:
_playlistTracks = _tracks.Where(t => t.Album.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
case CommonSongProperty.Artist:
_playlistTracks = _tracks.Where(t => t.Artist.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
default:
break;
}
}
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
}
public void ApplySongSearchQuery()
{
if (string.IsNullOrWhiteSpace(SongSearchQuery))
{
_filteredTracks = _playlistTracks;
return;
}
_filteredTracks = _playlistTracks.Where(t =>
t.Title.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Artist.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Album.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase)).ToList();
}
private void ApplySongOrderType()
{
switch (SongOrderType)
{
case CommonSongProperty.Title:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Title),
o => ((Track)o).Title
);
break;
case CommonSongProperty.Artist:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Artist),
o => ((Track)o).Artist
);
break;
case CommonSongProperty.Album:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Album),
o => ((Track)o).Album
);
break;
}
}
public void UpdateSelectedPlaylist(SongsTabInfo playlist)
{
var found = SongsTabInfoList.Where(x => x.FilterProperty == playlist.FilterProperty && x.FilterValue == playlist.FilterValue)
.ToList().FirstOrDefault();
if (found == null)
{
SongsTabInfoList.Add(playlist);
SelectedSongsTabInfoIndex = SongsTabInfoList.Count - 1;
}
else
{
SelectedSongsTabInfoIndex = SongsTabInfoList.IndexOf(found);
}
ApplyPlaylist();
}
public void PlayTrackAt(int index)
{
PlayTrack(TrackPlayingQueue.ElementAtOrDefault(index));
}
public void PlayTrack(PlayQueueItem? playQueueItem)
{
_timelineController.Pause();
_mediaPlayer.Source = null;
if (playQueueItem == null)
{
_smtc.IsEnabled = false;
}
else
{
var track = playQueueItem.Track;
var updater = _smtc.DisplayUpdater;
_smtc.IsEnabled = true;
_mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(track.Path));
updater.AppMediaId = Package.Current.Id.FullName;
updater.Type = MediaPlaybackType.Music;
updater.MusicProperties.Title = track.Title;
updater.MusicProperties.Artist = track.Artist;
updater.MusicProperties.AlbumTitle = track.Album;
if (track.EmbeddedPictures.FirstOrDefault()?.PictureData is byte[] pictureData)
{
updater.Thumbnail = ImageHelper.ByteArrayToRandomAccessStreamReference(pictureData);
}
else
{
updater.Thumbnail = null;
}
updater.Update();
}
}
partial void OnSongOrderTypeChanged(CommonSongProperty value)
{
ApplySongOrderType();
IsLocalMediaNotFound = !_filteredTracks.Any();
}
partial void OnSongSearchQueryChanged(string value)
{
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
}
partial void OnPlayingSongIndexChanged(int value)
{
DisplayedPlayingSongIndex = value + 1;
}
partial void OnPlaybackOrderChanged(PlaybackOrder value)
{
_settingsService.PlaybackOrder = value;
}
public void Receive(PropertyChangedMessage<ObservableCollection<LocalMediaFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalMediaFolders))
{
RefreshSongs();
}
}
}
}
}