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.
This commit is contained in:
Zhe Fang
2025-07-27 23:27:04 -04:00
parent 0e96c35d2d
commit 45eb2a1506
26 changed files with 388 additions and 99 deletions

View File

@@ -20,6 +20,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3
{
@@ -36,6 +37,8 @@ namespace BetterLyrics.WinUI3
public NotificationPanel? LyricsWindowNotificationPanel { get; set; }
public NotificationPanel? SettingsWindowNotificationPanel { get; set; }
private static Mutex? _instanceMutex;
public App()
{
this.InitializeComponent();
@@ -44,6 +47,8 @@ namespace BetterLyrics.WinUI3
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
EnsureSingleInstance();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
ConfigureServices();
@@ -56,6 +61,18 @@ namespace BetterLyrics.WinUI3
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void EnsureSingleInstance()
{
bool createdNew;
_instanceMutex = new Mutex(true, MetadataHelper.AppName, out createdNew);
if (!createdNew)
{
User32.MessageBox(HWND.NULL, ResourceLoader!.GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
}
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum SongOrderType
public enum CommonSongProperty
{
Title,
Album,

View File

@@ -29,6 +29,7 @@ namespace BetterLyrics.WinUI3.Helper
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string QQGroupUrl = "https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info";
public const string DiscordUrl = "https://discord.gg/5yAQPnyCKv";
public const string TelegramUrl = "https://t.me/+svhSLZ7awPsxNGY1";
public static async Task<DateTime> GetBuildDate()
{

View File

@@ -26,36 +26,39 @@ namespace BetterLyrics.WinUI3.Models
LyricsLines = lyricsLines;
}
public void SetDisplayedTextAlongWith(LyricsData translationData)
public void SetDisplayedTextAlongWith(LyricsData translationData, int toleranceMs = 0)
{
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= translationData.LyricsLines.Count)
// 在翻译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = translationData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
line.DisplayedText = line.OriginalText; // No translation available, keep original text
}
else
{
if (translationData.LanguageCode?.Substring(0, 2) == "zh")
if (translationData.LanguageCode?.StartsWith("zh") == true)
{
string tmp = "";
if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hant")
{
tmp = ChineseConverter.ConvertToTraditionalChinese(translationData.LyricsLines[i].OriginalText);
tmp = ChineseConverter.ConvertToTraditionalChinese(transLine.OriginalText);
}
else if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hans")
{
tmp = ChineseConverter.ConvertToSimplifiedChinese(translationData.LyricsLines[i].OriginalText);
tmp = ChineseConverter.ConvertToSimplifiedChinese(transLine.OriginalText);
}
line.DisplayedText = $"{line.OriginalText}\n{tmp}";
}
else
{
line.DisplayedText = $"{line.OriginalText}\n{translationData.LyricsLines[i].OriginalText}";
line.DisplayedText = $"{line.OriginalText}\n{transLine.OriginalText}";
}
}
i++;
else
{
// 没有匹配的翻译,翻译部分留空
line.DisplayedText = $"{line.OriginalText}\n";
}
}
}

View File

@@ -0,0 +1,19 @@
using ATL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class PlayQueueItem
{
public Track Track { get; set; }
public PlayQueueItem(Track track)
{
Track = track;
}
}
}

View File

@@ -0,0 +1,35 @@
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Models
{
public class SongsTabInfo
{
public string Name { get; set; }
public string Icon { get; set; }
public bool IsClosable { get; set; }
public CommonSongProperty FilterProperty { get; set; }
public string FilterValue { get; set; }
public SongsTabInfo()
{
Name = string.Empty;
Icon = string.Empty;
IsClosable = true;
FilterProperty = CommonSongProperty.Title;
FilterValue = string.Empty;
}
public SongsTabInfo(string name, string icon, bool isClosable, CommonSongProperty filterProperty, string filterValue)
{
Name = name;
Icon = icon;
IsClosable = isClosable;
FilterProperty = filterProperty;
FilterValue = filterValue;
}
}
}

View File

@@ -348,7 +348,7 @@ namespace BetterLyrics.WinUI3.Services
else if (result is NeteaseSearchResult neteaseResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(neteaseResult.Id);
var translated = response?.Tlyric.Lyric;
var translated = response?.Tlyric?.Lyric;
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(

View File

@@ -869,13 +869,13 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<value>List loop</value>
</data>
<data name="MusicGalleryPageSingleLoop.Content" xml:space="preserve">
<value>Single Loop</value>
<value>Single loop</value>
</data>
<data name="MusicGalleryPageQueueRandom.Content" xml:space="preserve">
<value>random</value>
<value>Random</value>
</data>
<data name="MusicGalleryPageRemoveFromPlayingQueue.Text" xml:space="preserve">
<value>Remove from the Play queue</value>
<value>Remove from play queue</value>
</data>
<data name="MusicGalleryPagePlayingQueueEmpty.Text" xml:space="preserve">
<value>Play queue is empty</value>
@@ -901,4 +901,13 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Open music gallery</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics is already running</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>All songs</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
</root>

View File

@@ -901,4 +901,13 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>オープンミュージックギャラリー</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics はすでに実行されています!</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>すべての曲</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
</root>

View File

@@ -901,4 +901,13 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>오픈 음악 갤러리</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 가 이미 실행 중입니다</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>모든 노래</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
</root>

View File

@@ -901,4 +901,13 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>打开音乐库</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 已经在运行</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有歌曲</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
</root>

View File

@@ -901,4 +901,13 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>開啟音樂庫</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 已經在運作</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有歌曲</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
</root>

View File

@@ -10,15 +10,15 @@ public class SongOrderTemplateSelector : DataTemplateSelector
public DataTemplate ByAlbumTemplate { get; set; }
public DataTemplate ByArtistTemplate { get; set; }
public SongOrderType SongOrderType { get; set; }
public CommonSongProperty SongOrderType { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
return SongOrderType switch
{
SongOrderType.Title => ByTitleTemplate,
SongOrderType.Album => ByAlbumTemplate,
SongOrderType.Artist => ByArtistTemplate,
CommonSongProperty.Title => ByTitleTemplate,
CommonSongProperty.Album => ByAlbumTemplate,
CommonSongProperty.Artist => ByArtistTemplate,
_ => ByTitleTemplate
};
}

View File

@@ -193,6 +193,7 @@ namespace BetterLyrics.WinUI3.ViewModels
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverAcrylicEffectAmount))
{
_coverAcrylicEffectAmount = message.NewValue;
_isCoverAcrylicEffectAmountChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsVerticalEdgeOpacity))
{

View File

@@ -74,6 +74,11 @@ namespace BetterLyrics.WinUI3.ViewModels
_titleY = _albumArtY + _albumArtSize * 1.05f;
_isCoverAcrylicEffectAmountChanged = true;
}
if (_isCoverAcrylicEffectAmountChanged)
{
UpdateCoverAcrylicOverlay(control);
}
@@ -446,14 +451,18 @@ namespace BetterLyrics.WinUI3.ViewModels
private void UpdateCoverAcrylicOverlay(ICanvasAnimatedControl control)
{
var ret = NoiseOverlayHelper.GenerateNoiseBitmapBGRA((int)_canvasWidth, (int)_canvasHeight);
_coverAcrylicNoiseCanvasBitmap = CanvasBitmap.CreateFromBytes(
control,
ret,
(int)_canvasWidth,
(int)_canvasHeight,
Windows.Graphics.DirectX.DirectXPixelFormat.B8G8R8A8UIntNormalized
);
if (_coverAcrylicEffectAmount > 0)
{
var ret = NoiseOverlayHelper.GenerateNoiseBitmapBGRA((int)_canvasWidth, (int)_canvasHeight);
_coverAcrylicNoiseCanvasBitmap = CanvasBitmap.CreateFromBytes(
control,
ret,
(int)_canvasWidth,
(int)_canvasHeight,
Windows.Graphics.DirectX.DirectXPixelFormat.B8G8R8A8UIntNormalized
);
}
_isCoverAcrylicEffectAmountChanged = false;
}
}
}

View File

@@ -47,6 +47,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private CanvasBitmap? _albumArtCanvasBitmap = null;
private CanvasBitmap? _coverAcrylicNoiseCanvasBitmap = null;
private bool _isCoverAcrylicEffectAmountChanged = false;
private float _albumArtSize = 0f;
private int _albumArtCornerRadius = 0;
@@ -444,7 +445,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(_lyricsDataArr[found]);
_lyricsDataArr[0].SetDisplayedTextAlongWith(_lyricsDataArr[found], 50);
_langIndex = 0;
}
}
@@ -550,7 +551,6 @@ namespace BetterLyrics.WinUI3.ViewModels
{
foreach (var data in translationData)
{
data.LyricsLines = data.LyricsLines.Where(line => !string.IsNullOrWhiteSpace(line.OriginalText)).ToList();
foreach (var item in data.LyricsLines)
{
if (item.OriginalText == "//")

View File

@@ -30,12 +30,19 @@ namespace BetterLyrics.WinUI3.ViewModels
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; } = [];
@@ -43,15 +50,23 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial List<Track> SelectedTracks { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<Track> TrackPlayingQueue { get; set; } = [];
public partial ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; } = [];
public Track? PlayingTrack => TrackPlayingQueue.ElementAtOrDefault(PlayingSongIndex);
public PlayQueueItem? PlayingQueueItem => TrackPlayingQueue.ElementAtOrDefault(PlayingSongIndex);
[ObservableProperty]
public partial PlaybackOrder PlaybackOrder { get; set; }
[ObservableProperty]
public partial SongOrderType SongOrderType { get; set; } = SongOrderType.Title;
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;
@@ -70,6 +85,8 @@ namespace BetterLyrics.WinUI3.ViewModels
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;
@@ -111,7 +128,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
PlayingSongIndex = 0;
}
PlayTrack(PlayingTrack);
PlayTrack(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
@@ -124,7 +141,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
PlayingSongIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
PlayTrack(PlayingTrack);
PlayTrack(PlayingQueueItem);
});
break;
default:
@@ -147,7 +164,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
PlayingSongIndex = TrackPlayingQueue.Count - 1;
}
PlayTrack(PlayingTrack);
PlayTrack(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
@@ -160,7 +177,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
PlayingSongIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
PlayTrack(PlayingTrack);
PlayTrack(PlayingQueueItem);
});
break;
default:
@@ -236,10 +253,10 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
ApplySongSearchQuery();
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
ApplyPlaylist();
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
IsDataLoading = false;
@@ -248,37 +265,64 @@ namespace BetterLyrics.WinUI3.ViewModels
}, 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 = _tracks;
_filteredTracks = _playlistTracks;
return;
}
_filteredTracks = new List<Track>(
_tracks.Where(t => t.Title.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Artist.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Album.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase))
);
_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 SongOrderType.Title:
case CommonSongProperty.Title:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Title),
o => ((Track)o).Title
);
break;
case SongOrderType.Artist:
case CommonSongProperty.Artist:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Artist),
o => ((Track)o).Artist
);
break;
case SongOrderType.Album:
case CommonSongProperty.Album:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Album),
o => ((Track)o).Album
@@ -287,21 +331,38 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
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(Track? track)
public void PlayTrack(PlayQueueItem? playQueueItem)
{
_timelineController.Pause();
_mediaPlayer.Source = null;
if (track == 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));
@@ -322,7 +383,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
partial void OnSongOrderTypeChanged(SongOrderType value)
partial void OnSongOrderTypeChanged(CommonSongProperty value)
{
ApplySongOrderType();
IsLocalMediaNotFound = !_filteredTracks.Any();

View File

@@ -102,11 +102,6 @@ namespace BetterLyrics.WinUI3.ViewModels
IsDragEverywhereEnabled = _settingsService.IsDragEverywhereEnabled;
_playbackService.MediaSourceProvidersInfoChanged += PlaybackService_SessionIdsChanged;
Task.Run(async () =>
{
BuildDate = (await MetadataHelper.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
});
}
private void PlaybackService_SessionIdsChanged(object? sender, Events.MediaSourceProvidersInfoEventArgs e)
@@ -281,8 +276,6 @@ namespace BetterLyrics.WinUI3.ViewModels
public string Version { get; set; } = MetadataHelper.AppVersion;
public string BuildDate { get; set; } = string.Empty;
[ObservableProperty]
public partial string LibreTranslateServer { get; set; }

View File

@@ -17,7 +17,7 @@
mc:Ignorable="d">
<Page.Resources>
<CollectionViewSource
x:Name="TracksByTitleCVS"
x:Name="GroupedTracksCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.GroupedTracks, Mode=OneWay}" />
</Page.Resources>
@@ -28,7 +28,10 @@
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="SongViewer" Grid.Column="0">
<Grid
x:Name="SongViewer"
Grid.Column="0"
Padding="12">
<Grid.Tag>
<Flyout
x:Name="SongFileInfoFlyout"
@@ -88,7 +91,58 @@
</Flyout>
</Grid.Tag>
<Grid Margin="12" VerticalAlignment="Top">
<ListView
VerticalAlignment="Top"
ItemsSource="{x:Bind ViewModel.SongsTabInfoList, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
SelectedIndex="{x:Bind ViewModel.SelectedSongsTabInfoIndex, Mode=TwoWay}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:SongsTabInfo">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Background="Transparent"
ColumnSpacing="6"
Tapped="PlaylistGrid_Tapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="{Binding Icon}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Name}" />
</Grid>
<Button
Grid.Column="1"
Click="PlaylistCloseButton_Click"
Content="{ui:FontIcon FontSize=10,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8BB;}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{Binding IsClosable, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Margin="0,48,0,0" VerticalAlignment="Top">
<AutoSuggestBox
x:Name="SongSearchBox"
x:Uid="MusicGalleryPageSongSearchBox"
@@ -97,7 +151,7 @@
Text="{x:Bind ViewModel.SongSearchQuery, Mode=TwoWay}" />
</Grid>
<Grid Margin="12,58,12,0" VerticalAlignment="Top">
<Grid Margin="0,96,0,0" VerticalAlignment="Top">
<StackPanel
HorizontalAlignment="Left"
Orientation="Horizontal"
@@ -143,12 +197,11 @@
</StackPanel>
</Grid>
<SemanticZoom Margin="0,96,0,0">
<SemanticZoom Margin="0,140,0,0">
<SemanticZoom.ZoomedInView>
<ListView
x:Name="SongListView"
Margin="12,0"
ItemsSource="{x:Bind TracksByTitleCVS.View, Mode=OneWay}"
ItemsSource="{x:Bind GroupedTracksCVS.View, Mode=OneWay}"
SelectionChanged="SongListView_SelectionChanged"
SelectionMode="Multiple">
<ListView.ItemTemplate>
@@ -173,7 +226,8 @@
<HyperlinkButton
Grid.Column="1"
VerticalAlignment="Center"
IsEnabled="False">
Click="ArtistHyperlibkButton_Click"
Tag="{Binding Artist}">
<TextBlock Text="{Binding Artist}" TextWrapping="Wrap" />
</HyperlinkButton>
@@ -181,7 +235,8 @@
<HyperlinkButton
Grid.Column="2"
VerticalAlignment="Center"
IsEnabled="False">
Click="AlbumHyperlibkButton_Click"
Tag="{Binding Album}">
<TextBlock Text="{Binding Album}" TextWrapping="Wrap" />
</HyperlinkButton>
@@ -216,7 +271,7 @@
<Border AutomationProperties.AccessibilityView="Raw">
<TextBlock
AutomationProperties.AccessibilityView="Raw"
Style="{ThemeResource TitleTextBlockStyle}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{Binding}" />
</Border>
</DataTemplate>
@@ -231,7 +286,7 @@
MaxWidth="500"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{x:Bind TracksByTitleCVS.View.CollectionGroups, Mode=OneWay}"
ItemsSource="{x:Bind GroupedTracksCVS.View.CollectionGroups, Mode=OneWay}"
ScrollViewer.IsHorizontalScrollChainingEnabled="False"
SelectionMode="None">
<GridView.ItemTemplate>
@@ -243,10 +298,16 @@
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
<Grid Margin="0,48,0,0" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Image MaxWidth="200" Source="/Assets/Planet.png" />
<TextBlock x:Uid="MusicGalleryPageFileNotFound" HorizontalAlignment="Center" />
<Grid Margin="0,140,0,0" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="200" Source="/Assets/EmptyState.png" />
<TextBlock
x:Uid="MusicGalleryPageFileNotFound"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
@@ -289,10 +350,10 @@
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel>
<TextBlock Text="{Binding Title}" TextWrapping="Wrap" />
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Text="{Binding Artist}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
@@ -328,9 +389,15 @@
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Image MaxWidth="200" Source="/Assets/Planet.png" />
<TextBlock x:Uid="MusicGalleryPagePlayingQueueEmpty" HorizontalAlignment="Center" />
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>

View File

@@ -1,6 +1,7 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI;
@@ -51,13 +52,9 @@ namespace BetterLyrics.WinUI3.Views
private void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var frameworkElement = ((FrameworkElement)sender).Parent;
var index = PlayingQueueListView.FindChildIndex(frameworkElement);
ViewModel.PlayTrackAt(index);
DispatcherQueue.TryEnqueue(async () =>
{
await PlayingQueueListView.SmoothScrollIntoViewWithIndexAsync(index, ScrollItemPlacement.Center);
});
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
ViewModel.PlayTrack(item);
PlayingQueueListView.ScrollIntoView(item);
}
private void EmptyPlayingQueueButton_Click(object sender, RoutedEventArgs e)
@@ -69,27 +66,32 @@ namespace BetterLyrics.WinUI3.Views
private void ScrollToPlayingItemButton_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.PlayingTrack == null) return;
DispatcherQueue.TryEnqueue(async () =>
{
await PlayingQueueListView.SmoothScrollIntoViewWithIndexAsync(ViewModel.PlayingSongIndex, ScrollItemPlacement.Center);
});
if (ViewModel.PlayingQueueItem == null) return;
PlayingQueueListView.ScrollIntoView(ViewModel.PlayingQueueItem);
}
private void RemoveFromPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
var frameworkElement = ((FrameworkElement)((FrameworkElement)sender).Parent).Parent;
bool playNext = false;
int index = PlayingQueueListView.FindChildIndex(frameworkElement);
if (index == ViewModel.PlayingSongIndex)
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
int index = ViewModel.TrackPlayingQueue.IndexOf(item);
if (item == ViewModel.PlayingQueueItem)
{
playNext = true;
}
ViewModel.TrackPlayingQueue.RemoveAt(index);
ViewModel.TrackPlayingQueue.Remove(item);
if (playNext)
{
if (ViewModel.TrackPlayingQueue.Count == 0)
{
index = -1;
}
else if (index >= ViewModel.TrackPlayingQueue.Count)
{
index = ViewModel.TrackPlayingQueue.Count - 1;
}
ViewModel.PlayingSongIndex = index;
ViewModel.PlayTrackAt(index);
ViewModel.PlayTrackAt(ViewModel.PlayingSongIndex);
}
}
@@ -101,7 +103,7 @@ namespace BetterLyrics.WinUI3.Views
private void AddSongToQueueNextMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.PlayingSongIndex + 1, SongListView.SelectedItems.Cast<Track>());
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.PlayingSongIndex + 1, SongListView.SelectedItems.Cast<Track>().Select(x => new PlayQueueItem(x)));
if (startPlaying)
{
ViewModel.PlayingSongIndex = ViewModel.PlayingSongIndex + 1;
@@ -112,7 +114,7 @@ namespace BetterLyrics.WinUI3.Views
private void AddSongToQueueEndMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
ViewModel.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<Track>());
ViewModel.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<Track>().Select(x => new PlayQueueItem(x)));
if (startPlaying)
{
ViewModel.PlayingSongIndex = ViewModel.PlayingSongIndex + 1;
@@ -137,5 +139,33 @@ namespace BetterLyrics.WinUI3.Views
SongListView.SelectedItems.Clear();
}
}
private void ArtistHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var artist = (string)((HyperlinkButton)sender).Tag;
var playlist = new SongsTabInfo(artist, "\uEFA9", true, CommonSongProperty.Artist, artist);
ViewModel.UpdateSelectedPlaylist(playlist);
}
private void AlbumHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var album = (string)((HyperlinkButton)sender).Tag;
var playlist = new SongsTabInfo(album, "\uE93C", true, CommonSongProperty.Album, album);
ViewModel.UpdateSelectedPlaylist(playlist);
}
private void PlaylistGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var playlist = (SongsTabInfo)((FrameworkElement)sender).DataContext;
ViewModel.UpdateSelectedPlaylist(playlist);
}
private void PlaylistCloseButton_Click(object sender, RoutedEventArgs e)
{
var playlist = (SongsTabInfo)((FrameworkElement)sender).DataContext;
ViewModel.SongsTabInfoList.Remove(playlist);
ViewModel.SelectedSongsTabInfoIndex = 0;
ViewModel.ApplyPlaylist();
}
}
}

View File

@@ -935,7 +935,6 @@
<Paragraph>
<Run x:Uid="SettingsPageVersion" />
<Run Text="{x:Bind ViewModel.Version, Mode=OneWay}" />
<Run Text="{x:Bind ViewModel.BuildDate, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</controls:SettingsCard.Description>
@@ -962,6 +961,10 @@
<Button x:Uid="SettingsPageJoinNowButton" Click="DiscodGroupButton_Click" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageTelegram" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/Telegram.png}">
<Button x:Uid="SettingsPageJoinNowButton" Click="TelegramGroupButton_Click" />
</controls:SettingsCard>
</StackPanel>
</controls:Case>

View File

@@ -131,5 +131,10 @@ namespace BetterLyrics.WinUI3.Views
{
Launcher.LaunchUriAsync(new Uri(MetadataHelper.DiscordUrl));
}
private void TelegramGroupButton_Click(object sender, RoutedEventArgs e)
{
Launcher.LaunchUriAsync(new Uri(MetadataHelper.TelegramUrl));
}
}
}