chores: adjust layout and fix bugs

This commit is contained in:
Zhe Fang
2025-12-28 20:01:41 -05:00
parent 83f3a3bd6d
commit b0a777db8d
42 changed files with 1508 additions and 726 deletions

View File

@@ -97,7 +97,7 @@
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="14,6,14,9" />
<Setter Property="Padding" Value="16,9,16,9" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -170,6 +170,9 @@
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@@ -50,21 +50,24 @@
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:MediaFolder">
<dev:SettingsExpander Description="{x:Bind ConnectionSummary, Mode=OneWay}" IsExpanded="True">
<dev:SettingsExpander IsExpanded="True">
<dev:SettingsExpander.HeaderIcon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="{x:Bind SourceType, Converter={StaticResource FileSourceTypeToIconConverter}, Mode=OneWay}" />
</dev:SettingsExpander.HeaderIcon>
<dev:SettingsExpander.Header>
<HyperlinkButton
Padding="0"
Click="LocalFolderHyperlinkButton_Click"
Content="{x:Bind ConnectionSummary, Mode=OneWay}" />
<TextBlock IsTextSelectionEnabled="True" Text="{x:Bind Name, Mode=OneWay}" />
</dev:SettingsExpander.Header>
<dev:SettingsExpander.Description>
<TextBlock IsTextSelectionEnabled="True" Text="{x:Bind ConnectionSummary, Mode=OneWay}" />
</dev:SettingsExpander.Description>
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="MediaSettingsControlNameSetting">
<TextBox VerticalAlignment="Center" Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="MediaSettingsControlLastSyncTime" Description="{x:Bind LastSyncTime.ToString(), Mode=OneWay, TargetNullValue=N/A}">
<Button
x:Uid="MediaSettingsControlSyncNow"
@@ -128,14 +131,17 @@
<MenuFlyout>
<MenuFlyoutItem
x:Uid="SettingsPageLocalFolder"
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
CommandParameter="{Binding ElementName=RootGrid}"
Icon="Folder" />
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="Local">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE8B7;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="SMB"
Text="SMB">
<MenuFlyoutItem.Icon>
@@ -144,7 +150,7 @@
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="FTP"
Text="FTP">
<MenuFlyoutItem.Icon>
@@ -153,7 +159,7 @@
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="WebDAV"
Text="WebDAV">
<MenuFlyoutItem.Icon>

View File

@@ -27,15 +27,6 @@ namespace BetterLyrics.WinUI3.Controls
ViewModel.RemoveFolder(folder);
}
private async void LocalFolderHyperlinkButton_Click(object sender, RoutedEventArgs e)
{
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
if (Uri.TryCreate(folder.UriString, UriKind.Absolute, out var uri))
{
await Launcher.LaunchUriAsync(uri);
}
}
private void SyncNowButton_Click(object sender, RoutedEventArgs e)
{
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;

View File

@@ -9,56 +9,85 @@
mc:Ignorable="d">
<Grid>
<StackPanel Width="400" Spacing="16">
<ProgressBar
x:Name="ProgressBar"
IsIndeterminate="True"
Visibility="Collapsed" />
<InfoBar
x:Name="ErrorInfoBar"
IsClosable="True"
IsOpen="False"
Severity="Error" />
<ScrollViewer>
<StackPanel Width="400" Spacing="16">
<ProgressBar
x:Name="ProgressBar"
IsIndeterminate="True"
Visibility="Collapsed" />
<InfoBar
x:Name="ErrorInfoBar"
IsClosable="True"
IsOpen="False"
Severity="Error" />
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="12">
<TextBox
x:Name="HostBox"
x:Uid="RemoteServerConfigControlServerAddress"
Grid.Column="0"
InputScope="Url"
PlaceholderText="192.168.1.x"
x:Name="NameBox"
x:Uid="RemoteServerConfigControlName"
TextWrapping="Wrap" />
<NumberBox
x:Name="PortBox"
x:Uid="RemoteServerConfigControlPort"
Grid.Column="1"
MinWidth="100"
LargeChange="10"
SmallChange="1"
SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="80"
Value="80" />
</Grid>
<StackPanel x:Name="RemoteFieldsPanel" Spacing="16">
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="12">
<TextBox
x:Name="HostBox"
x:Uid="RemoteServerConfigControlServerAddress"
Grid.Column="0"
InputScope="Url"
PlaceholderText="192.168.1.x"
TextWrapping="Wrap" />
<TextBox
x:Name="PathBox"
x:Uid="RemoteServerConfigControlPath"
TextWrapping="Wrap" />
<NumberBox
x:Name="PortBox"
x:Uid="RemoteServerConfigControlPort"
Grid.Column="1"
MinWidth="100"
LargeChange="10"
SmallChange="1"
SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="80"
Value="80" />
</Grid>
</StackPanel>
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<TextBox
x:Name="UserBox"
x:Uid="RemoteServerConfigControlUsername"
Grid.Column="0"
TextWrapping="Wrap" />
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="8">
<TextBox
x:Name="PathBox"
x:Uid="RemoteServerConfigControlPath"
Grid.Column="0"
TextChanged="PathBox_TextChanged"
TextWrapping="Wrap" />
<PasswordBox
x:Name="PwdBox"
x:Uid="RemoteServerConfigControlPassword"
Grid.Column="1"
PasswordRevealMode="Peek" />
</Grid>
</StackPanel>
<Button
x:Name="BrowseButton"
x:Uid="RemoteServerConfigControlBrowse"
Grid.Column="1"
VerticalAlignment="Bottom"
Click="BrowseButton_Click"
Visibility="Collapsed" />
</Grid>
<InfoBar
x:Name="PathWarningBar"
IsClosable="False"
IsOpen="False"
Severity="Warning" />
<StackPanel x:Name="AuthFieldsPanel" Spacing="16">
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<TextBox
x:Name="UserBox"
x:Uid="RemoteServerConfigControlUsername"
Grid.Column="0"
TextWrapping="Wrap" />
<PasswordBox
x:Name="PwdBox"
x:Uid="RemoteServerConfigControlPassword"
Grid.Column="1"
PasswordRevealMode="Peek" />
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -1,6 +1,8 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -19,24 +21,41 @@ namespace BetterLyrics.WinUI3.Controls
_protocolType = protocolType;
SetupDefaults();
CheckPathForWarning();
}
private void SetupDefaults()
{
switch (_protocolType.ToUpper())
if (_protocolType.Equals("Local", StringComparison.OrdinalIgnoreCase))
{
case "SMB":
PortBox.Value = 445; // SMB Ĭ<>϶˿<CFB6>
PathBox.PlaceholderText = "SharedMusic";
break;
case "FTP":
PortBox.Value = 21; // FTP Ĭ<>϶˿<CFB6>
PathBox.PlaceholderText = "/pub/music";
break;
case "WEBDAV":
PortBox.Value = 80; // WebDAV Ĭ<>϶˿<CFB6>
PathBox.PlaceholderText = "/dav/music";
break;
RemoteFieldsPanel.Visibility = Visibility.Collapsed;
AuthFieldsPanel.Visibility = Visibility.Collapsed;
BrowseButton.Visibility = Visibility.Visible;
PathBox.PlaceholderText = @"D:\Music";
}
else
{
BrowseButton.Visibility = Visibility.Collapsed;
RemoteFieldsPanel.Visibility = Visibility.Visible;
AuthFieldsPanel.Visibility = Visibility.Visible;
switch (_protocolType.ToUpper())
{
case "SMB":
PortBox.Value = 445;
PathBox.PlaceholderText = "SharedMusic";
break;
case "FTP":
PortBox.Value = 21;
PathBox.PlaceholderText = "/pub/music";
break;
case "WEBDAV":
PortBox.Value = 80;
PathBox.PlaceholderText = "/dav/music";
break;
}
}
}
@@ -60,17 +79,46 @@ namespace BetterLyrics.WinUI3.Controls
public MediaFolder GetConfig()
{
string finalName = HostBox.Text.Trim();
if (_protocolType.Equals("Local", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(PathBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathRequired"));
if (!string.IsNullOrWhiteSpace(NameBox.Text))
finalName = NameBox.Text.Trim();
else
finalName = PathBox.Text.TrimEnd(System.IO.Path.DirectorySeparatorChar);
return new MediaFolder
{
Name = finalName,
SourceType = FileSourceType.Local,
UriScheme = "file",
UriPath = PathBox.Text.Trim(),
};
}
if (string.IsNullOrWhiteSpace(HostBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlServerAddressRequired"));
string name = $"{_protocolType} - {HostBox.Text}";
if (!string.IsNullOrWhiteSpace(NameBox.Text))
{
finalName = NameBox.Text.Trim();
}
else
{
finalName = $"{_protocolType} - {HostBox.Text}";
}
Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
string scheme = GetScheme();
var folder = new MediaFolder
{
Name = name,
Name = finalName,
SourceType = sourceType,
UriScheme = scheme,
@@ -86,10 +134,10 @@ namespace BetterLyrics.WinUI3.Controls
return folder;
}
public void ShowError(string message)
public void ShowError(string? message)
{
ErrorInfoBar.Message = message;
ErrorInfoBar.IsOpen = true;
ErrorInfoBar.IsOpen = !string.IsNullOrWhiteSpace(message);
}
public void SetProgressBarVisibility(Visibility visibility)
@@ -97,5 +145,53 @@ namespace BetterLyrics.WinUI3.Controls
ProgressBar.Visibility = visibility;
}
private void PathBox_TextChanged(object sender, TextChangedEventArgs e)
{
CheckPathForWarning();
}
private void CheckPathForWarning()
{
string? path = PathBox.Text?.Trim();
bool isSymbolRoot = string.IsNullOrEmpty(path) ||
path == "/" ||
path == "\\";
bool isDriveRoot = false;
if (!string.IsNullOrEmpty(path))
{
var normalized = path.TrimEnd('\\', '/');
isDriveRoot = normalized.EndsWith(":") && normalized.Length == 2;
}
bool isRoot = isSymbolRoot || isDriveRoot;
if (isRoot)
{
PathWarningBar.Message = _localizationService.GetLocalizedString("FileSystemServiceRootDirectoryWarning");
PathWarningBar.IsOpen = true;
}
else
{
PathWarningBar.IsOpen = false;
}
}
private async void BrowseButton_Click(object sender, RoutedEventArgs e)
{
try
{
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
if (folder != null)
{
PathBox.Text = folder.Path;
}
}
catch (Exception ex)
{
ShowError(ex.Message);
}
}
}
}

View File

@@ -18,13 +18,7 @@
<TextBlock x:Uid="AppSettingsControlGeneral" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageConfigName" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8AC;}">
<StackPanel
Margin="0,6,0,0"
Orientation="Horizontal"
Spacing="6">
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay}" TextWrapping="Wrap" />
<Button Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, FontSize=12, Glyph=&#xE8FB;}" Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
</dev:SettingsCard>
<dev:SettingsExpander

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using BetterLyrics.WinUI3.Models;
public static class FolderTreeBuilder
{
public static ObservableCollection<FolderNode> Build(List<ExtendedTrack> tracks, List<MediaFolder> folderConfigs)
{
var rootNodes = new ObservableCollection<FolderNode>();
// 按 MediaFolderId 分组
var folderGroups = tracks.GroupBy(t => t.MediaFolderId);
foreach (var group in folderGroups)
{
var config = folderConfigs.FirstOrDefault(f => f.Id == group.Key);
if (config == null) continue;
string baseUri = config.GetStandardUri().AbsoluteUri.TrimEnd('/');
var rootNode = new FolderNode
{
SourceType = config.SourceType,
FolderName = config.Name ?? config.ConnectionSummary, // 显示用户自定义的名字
MediaFolderId = group.Key,
FolderPath = baseUri,
IsExpanded = true
};
foreach (var track in group)
{
try
{
if (!track.Uri.StartsWith(baseUri)) continue; // 防御性编程
string relativePart = track.Uri.Substring(baseUri.Length);
var segments = relativePart
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(s => System.Net.WebUtility.UrlDecode(s))
.ToArray();
if (segments.Length > 1) // 长度大于1说明在子文件夹里
{
var folderSegments = segments.Take(segments.Length - 1).ToArray();
CreateFolderStructure(rootNode, folderSegments, baseUri);
}
}
catch { }
}
rootNodes.Add(rootNode);
}
return rootNodes;
}
private static void CreateFolderStructure(FolderNode parent, string[] segments, string rootBaseUri)
{
var current = parent;
string currentFullPath = parent.FolderPath;
foreach (var segmentName in segments)
{
var existingChild = current.SubFolders.FirstOrDefault(f => f.FolderName == segmentName);
currentFullPath += "/" + System.Net.WebUtility.UrlEncode(segmentName);
if (existingChild == null)
{
var newFolder = new FolderNode
{
FolderName = segmentName,
FolderPath = currentFullPath, // 存完整的 URI
MediaFolderId = parent.MediaFolderId
};
current.SubFolders.Add(newFolder);
current = newFolder;
}
else
{
current = existingChild;
currentFullPath = existingChild.FolderPath;
}
}
}
}

View File

@@ -8,20 +8,17 @@ namespace BetterLyrics.WinUI3.Models
{
public class ExtendedTrack
{
// 标准 URI (file:///..., smb://..., http://...)
public string Uri { get; private set; } = "";
// 对于本地文件,返回 C:\Music\Song.mp3
// 对于远程文件,返回解码后的路径部分 /Music/Song.mp3
public string UriPath
public string DecodedAbsoluteUri
{
get
{
if (string.IsNullOrEmpty(Uri)) return "";
try
{
var u = new System.Uri(Uri);
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsolutePath);
var u = new Uri(Uri);
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsoluteUri);
}
catch { return Uri; }
}
@@ -105,6 +102,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
}
public string MediaFolderId { get; set; } = "";
public string Title { get; set; } = "";
public string Artist { get; set; } = "";
@@ -142,6 +140,7 @@ namespace BetterLyrics.WinUI3.Models
{
if (entity == null) return;
this.MediaFolderId = entity.MediaFolderId;
this.Uri = entity.Uri;
this.Title = entity.Title;

View File

@@ -28,7 +28,7 @@ namespace BetterLyrics.WinUI3.Models
[Indexed(Unique = true)]
public string Uri { get; set; }
public string FileName { get; set; }
public string FileName { get; set; } = "";
public bool IsDirectory { get; set; }
@@ -39,17 +39,17 @@ namespace BetterLyrics.WinUI3.Models
public DateTime? LastModified { get; set; }
// ------ 元数据部分 (保持不变) ------
public string? Title { get; set; }
public string? Artists { get; set; }
public string? Album { get; set; }
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; } // 建议明确单位,例如 DurationMs
public string? AudioFormatName { get; set; }
public string? AudioFormatShortName { get; set; }
public string? Encoder { get; set; }
public int Duration { get; set; }
public string AudioFormatName { get; set; } = "";
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
public string? EmbeddedLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public bool IsMetadataParsed { get; set; }

View File

@@ -0,0 +1,24 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public partial class FolderNode : ObservableObject
{
public FileSourceType SourceType { get; set; } = FileSourceType.Local;
public string FolderName { get; set; } = "";
public string FolderPath { get; set; } = "";
public string MediaFolderId { get; set; } = "";
public ObservableCollection<FolderNode> SubFolders { get; set; } = new();
[ObservableProperty] public partial bool IsExpanded { get; set; }
}
}

View File

@@ -64,7 +64,6 @@ namespace BetterLyrics.WinUI3.Models
Scheme = UriScheme ?? "file",
Host = UriHost,
Port = UriPort,
UserName = UserName
};
if (!string.IsNullOrEmpty(UriPath))

View File

@@ -6,37 +6,18 @@ namespace BetterLyrics.WinUI3.Models
{
public partial class SongsTabInfo : BaseViewModel
{
public string Name { get; set; }
public string Name { get; set; } = "";
public string Icon { get; set; }
public string Icon { get; set; } = "";
public bool IsClosable { get; set; }
public CommonSongProperty FilterProperty { get; set; } = CommonSongProperty.Title;
[ObservableProperty]
public partial bool IsStarred { get; set; }
public string FilterValue { get; set; } = "";
public CommonSongProperty FilterProperty { get; set; }
public string FilterValue { get; set; }
public bool IsDefault => Icon == "\uE8A9";
public SongsTabInfo()
{
Name = string.Empty;
Icon = string.Empty;
IsClosable = true;
IsStarred = false;
FilterProperty = CommonSongProperty.Title;
FilterValue = string.Empty;
}
public SongsTabInfo(string name, string icon, bool isClosable, bool isStarred, CommonSongProperty filterProperty, string filterValue)
{
Name = name;
Icon = icon;
IsClosable = isClosable;
IsStarred = isStarred;
FilterProperty = filterProperty;
FilterValue = filterValue;
}
}
}

View File

@@ -88,6 +88,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
if (enabledIds.Count == 0) return null;
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FileCacheEntity? bestMatch = null;

View File

@@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Extensions.Logging;
using SQLite;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -28,7 +29,12 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
private readonly SQLiteAsyncConnection _db;
private bool _isInitialized = false;
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, CancellationTokenSource> _folderTimerTokens = new();
// 定时器字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _folderTimerTokens = new();
// 当前正在执行的扫描任务字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeScanTokens = new();
private static readonly SemaphoreSlim _dbLock = new(1, 1);
private static readonly SemaphoreSlim _folderScanLock = new(1, 1);
@@ -238,23 +244,34 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServicePrepareToClean");
folder.IsCleaningUp = true;
});
if (_folderTimerTokens.TryRemove(folder.Id, out var cts))
if (_folderTimerTokens.TryRemove(folder.Id, out var timerCts))
{
cts.Cancel();
cts.Dispose();
timerCts.Cancel();
timerCts.Dispose();
_logger.LogInformation("DeleteCacheForMediaFolderAsync: {}", "cts.Dispose();");
}
if (_activeScanTokens.TryGetValue(folder.Id, out var activeScanCts))
{
activeScanCts.Cancel();
// 强制终止正在扫描的操作
}
try
{
await _folderScanLock.WaitAsync();
try
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
});
await InitializeAsync();
await _dbLock.WaitAsync();
@@ -292,6 +309,9 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
if (folder == null || !folder.IsEnabled) return;
using var scanCts = CancellationTokenSource.CreateLinkedTokenSource(token);
_activeScanTokens[folder.Id] = scanCts;
_dispatcherQueue.TryEnqueue(() =>
{
folder.IsIndexing = true;
@@ -301,7 +321,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
try
{
await _folderScanLock.WaitAsync(token);
await _folderScanLock.WaitAsync(scanCts.Token);
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
@@ -322,7 +342,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
while (foldersToScan.Count > 0)
{
if (token.IsCancellationRequested) return;
if (scanCts.Token.IsCancellationRequested) return;
var currentParent = foldersToScan.Dequeue();
@@ -350,7 +370,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
foreach (var item in filesToProcess)
{
if (token.IsCancellationRequested) return;
if (scanCts.Token.IsCancellationRequested) return;
current++;
@@ -383,7 +403,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
else
{
using var memStream = new MemoryStream();
await originalStream.CopyToAsync(memStream, token);
await originalStream.CopyToAsync(memStream, scanCts.Token);
memStream.Position = 0;
track = new ExtendedTrack(item, memStream);
}
@@ -459,6 +479,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
_folderScanLock.Release();
_activeScanTokens.TryRemove(folder.Id, out _);
_dispatcherQueue.TryEnqueue(() =>
{
folder.IsIndexing = false;
@@ -481,8 +503,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
// SQL 逻辑: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
var results = await _db.Table<FileCacheEntity>()
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
.ToListAsync();
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
.ToListAsync();
return results;
}

View File

@@ -3,7 +3,8 @@ using FluentFTP;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net; // 用于 WebUtility.UrlDecode
using System.Text; // ★ 修复 Encoding 报错的关键
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
@@ -17,16 +18,16 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
_config = config ?? throw new ArgumentNullException(nameof(config));
// 初始化 FluentFTP 配置
var ftpConfig = new FtpConfig
{
ConnectTimeout = 5000,
// 根据需要配置编码,防止中文乱码
// Encoding = System.Text.Encoding.GetEncoding("GB2312")
DataConnectionConnectTimeout = 5000,
ReadTimeout = 10000,
// 忽略证书错误
ValidateAnyCertificate = true
};
// FluentFTP 构造函数接收主机、用户、密码、端口
// 端口如果为 -1 (MediaFolder 默认值),则让 FluentFTP 使用默认 21
int port = _config.UriPort > 0 ? _config.UriPort : 0;
_client = new AsyncFtpClient(
@@ -42,11 +43,13 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
try
{
await _client.AutoConnect();
if (_client.IsConnected) return true;
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
return _client.IsConnected;
}
catch
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"FTP连接失败: {ex.Message}");
return false;
}
}
@@ -55,98 +58,122 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
var result = new List<FileCacheEntity>();
// 1. 确定目标服务器路径
// 1. 确定 FTP 服务器上的绝对路径
string targetServerPath;
Uri parentUri;
if (parentFolder == null)
{
// 根目录:从配置中提取路径 (例如 /Music)
// GetStandardUri().AbsolutePath 会返回带前导斜杠的路径
// 根目录:从配置中提取
var rootUri = _config.GetStandardUri();
targetServerPath = rootUri.AbsolutePath; // "/Music"
targetServerPath = rootUri.AbsolutePath;
parentUri = rootUri;
}
else
{
// 子目录:将标准 URI 转换为 FTP 服务器路径
// 子目录:从实体中提取
targetServerPath = GetServerPathFromUri(parentFolder.Uri);
parentUri = new Uri(parentFolder.Uri);
}
// 确保路径合法性 (FluentFTP 喜欢 Unix 风格斜杠)
targetServerPath = targetServerPath.Replace("\\", "/");
// 2. 路径清洗:解码 URL (比如 %20 -> 空格),并统一分隔符
targetServerPath = WebUtility.UrlDecode(targetServerPath).Replace("\\", "/");
if (string.IsNullOrEmpty(targetServerPath)) targetServerPath = "/";
// 2. 获取列表
var items = await _client.GetListing(targetServerPath);
// 3. 准备 Base URI 用于拼接子项
// FTP URI 基础部分: ftp://host:port
string baseUriStr = $"{parentUri.Scheme}://{parentUri.Host}";
if (parentUri.Port > 0) baseUriStr += $":{parentUri.Port}";
foreach (var item in items)
try
{
// 排除 . 和 ..
if (item.Name == "." || item.Name == "..") continue;
// 3. 获取列表 (FluentFTP 自动处理列表解析)
var items = await _client.GetListing(targetServerPath, FtpListOption.Auto);
// 构建完整的标准 URI
// item.FullName 是服务器上的绝对路径 (例如 /Music/Song.mp3)
// 我们需要把它拼成 ftp://host:port/Music/Song.mp3
// 注意Path.Combine 在 Windows 上可能会用反斜杠,这里手动拼接更安全
// 准备 Base URI Scheme (ftp://192.168.1.5:21) 用于拼接子项
string baseUriSchema = $"{parentUri.Scheme}://{parentUri.Host}";
if (parentUri.Port > 0) baseUriSchema += $":{parentUri.Port}";
string itemFullPath = item.FullName.StartsWith("/") ? item.FullName : "/" + item.FullName;
string standardUri = baseUriStr + itemFullPath; // Uri 构造函数会自动处理编码
result.Add(new FileCacheEntity
foreach (var item in items)
{
MediaFolderId = _config.Id,
// 跳过 . 和 ..
if (item.Name == "." || item.Name == "..") continue;
// 记录父级 URI
// 如果 parentFolder 为空,则父级是 Config 的根 URI
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
// 只处理文件和文件夹
if (item.Type != FtpObjectType.File && item.Type != FtpObjectType.Directory) continue;
Uri = standardUri, // 标准 URI
// 4. 构建标准 URI
// FluentFTP 的 item.FullName 通常是 "/Music/Song.mp3"
// 我们用 UriBuilder 把它封装成 "ftp://192.168.1.5:21/Music/Song.mp3"
// UriBuilder 会自动处理路径中的特殊字符编码
var builder = new UriBuilder(baseUriSchema)
{
Path = item.FullName
};
FileName = item.Name,
IsDirectory = item.Type == FtpObjectType.Directory,
result.Add(new FileCacheEntity
{
MediaFolderId = _config.Id,
// 如果是根目录扫描ParentUri 用 Config 的;否则用传入文件夹的
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
FileSize = item.Size,
LastModified = item.Modified
});
Uri = builder.Uri.AbsoluteUri, // 标准化 URI
FileName = item.Name,
IsDirectory = item.Type == FtpObjectType.Directory,
FileSize = item.Size,
// 防止某些服务器返回 MinValue
LastModified = item.Modified == DateTime.MinValue ? DateTime.Now : item.Modified,
IsMetadataParsed = false
});
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"FTP列表获取失败: {targetServerPath} - {ex.Message}");
}
return result;
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
public async Task<Stream?> OpenReadAsync(FileCacheEntity file)
{
if (entity == null) return null;
if (file == null) return null;
// 从标准 URI 还原回 FTP 服务器路径
string serverPath = GetServerPathFromUri(entity.Uri);
try
{
// 1. 还原服务器路径
string serverPath = GetServerPathFromUri(file.Uri);
return await _client.OpenRead(serverPath);
// 2. 解码 (Uri 里的空格是 %20FTP 需要真实空格)
serverPath = WebUtility.UrlDecode(serverPath);
// 3. 返回流
// 注意FluentFTP 的 OpenRead 依赖于连接保持活跃
return await _client.OpenRead(serverPath);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"打开文件流失败: {file.FileName} - {ex.Message}");
return null;
}
}
public async Task DisconnectAsync() => await _client.Disconnect();
public void Dispose() => _client?.Dispose();
public async Task DisconnectAsync()
{
if (_client.IsConnected)
{
await _client.Disconnect();
}
}
// =========================================================
// ★ 私有辅助方法URI -> FTP Path
// =========================================================
public void Dispose()
{
_client?.Dispose();
GC.SuppressFinalize(this);
}
// 私有辅助方法
private string GetServerPathFromUri(string uriString)
{
// 输入: ftp://192.168.1.5:21/Music/Song.mp3
// 输出: /Music/Song.mp3
var uri = new Uri(uriString);
// Uri.AbsolutePath 自动包含了路径部分 (例如 /Music/Song.mp3)
// 并且会自动进行 URL Decode (比如 %20 -> 空格)
// 这正是 FluentFTP 需要的格式
return uri.AbsolutePath;
return uri.AbsolutePath; // 这里拿到的比如是 "/Music/Song%201.mp3"
}
}
}

View File

@@ -299,6 +299,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (enabledIds.Count == 0) return lyricsSearchResult;
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.LyricExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
foreach (var item in allFiles)
{
@@ -342,6 +343,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (enabledIds.Count == 0) return lyricsSearchResult;
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FileCacheEntity? bestFile = null;
int maxScore = 0;

View File

@@ -24,11 +24,13 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
public partial class SettingsService : BaseViewModel, ISettingsService
{
private readonly DispatcherQueueTimer _writeAppSettingsTimer;
private readonly ILocalizationService _localizationService;
public AppSettings AppSettings { get; set; }
public SettingsService()
public SettingsService(ILocalizationService localizationService)
{
_localizationService = localizationService;
_writeAppSettingsTimer = _dispatcherQueue.CreateTimer();
AppSettings = ReadAppSettings();
@@ -60,6 +62,7 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
AppSettings.Version = MetadataHelper.AppVersion;
EnsureMediaSourceProvidersInfo();
EnsureStarredPlaylists();
}
private void EnsureMediaSourceProvidersInfo()
@@ -102,6 +105,20 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
}
}
private void EnsureStarredPlaylists()
{
if (!AppSettings.StarredPlaylists.Any(x => x.IsDefault))
{
AppSettings.StarredPlaylists.Insert(0, new SongsTabInfo
{
Name = _localizationService.GetLocalizedString("MusicGalleryPageAllSongs"),
Icon = "\uE8A9",
FilterProperty = CommonSongProperty.Title,
FilterValue = string.Empty
});
}
}
private void AppSettings_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
WriteAppSettings();

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>لم يتم العثور على أغاني في مكتبة الوسائط</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>استيراد من ملف</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>سياسة الخصوصية</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>كلمة المرور</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>المسار</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>المنفذ</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Keine Songs in der Medienbibliothek gefunden</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Aus Datei importieren</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Datenschutzrichtlinie</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Pfad</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>

View File

@@ -180,8 +180,14 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparing to clean cache...</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>The root directory path has been detected. A full disk index may contain a large number of non-media files and cause the scan to take too long. It is recommended to specify a specific subdirectory.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Waiting for scan...</value>
<value>Preparing to scan...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Fullscreen Mode</value>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Last Sync Time</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Local Folder</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Sync now</value>
</data>
@@ -391,7 +403,7 @@
<value>Next item</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>Add to queue</value>
<value>Add to playing queue</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>All Music</value>
@@ -438,11 +450,14 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>No songs found in media library</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Folders</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Import from file</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Create Playlist</value>
<value>Create playlist</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Playing Queue</value>
@@ -451,7 +466,7 @@
<value>Playing queue is empty</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Playlist</value>
<value>Playlists</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Loop List</value>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Privacy Policy</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Browse</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Leaving it blank will automatically generate a default name.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Password</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Path</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>The specified folder path could not be found</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Path is required</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>No se encontraron canciones en la biblioteca multimedia</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Importar desde archivo</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Política de privacidad</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Contraseña</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Ruta</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Puerto</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Aucune chanson trouvée dans la bibliothèque multimédia</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Importer depuis un fichier</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Politique de confidentialité</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Mot de passe</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Chemin</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>मीडिया लाइब्रेरी में कोई गाना नहीं मिला</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>फ़ाइल से आयात करें</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>गोपनीयता नीति</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>पासवर्ड</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>पथ</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>पोर्ट</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Tidak ada lagu yang ditemukan di pustaka media</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Impor dari file</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Kebijakan Privasi</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Kata Sandi</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Jalur</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>メディアライブラリに曲が見つかりません</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>ファイルからインポート</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>個人情報保護方針</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>パスワード</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>パス</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>ポート</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>미디어 라이브러리에서 곡을 찾지 못했습니다</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>파일에서 가져오기</value>
</data>
@@ -528,12 +543,24 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>개인정보 처리방침</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>비밀번호</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>경로</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>포트</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Tiada lagu ditemui dalam pustaka media</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Import dari fail</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Dasar Privasi</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Kata Laluan</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Laluan</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Nenhuma música encontrada na biblioteca multimédia</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Importar de ficheiro</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Política de Privacidade</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Palavra-passe</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Caminho</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Porta</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Песни в медиатеке не найдены</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Импорт из файла</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Политика конфиденциальности</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Пароль</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Путь</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Порт</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>ไม่พบเพลงในไลบรารีสื่อ</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>นำเข้าจากไฟล์</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>นโยบายความเป็นส่วนตัว</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>รหัสผ่าน</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>เส้นทาง</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>พอร์ต</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Không tìm thấy bài hát nào trong thư viện phương tiện</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Nhập từ tệp</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Chính sách bảo mật</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Mật khẩu</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Đường dẫn</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Cổng</value>
</data>

View File

@@ -166,22 +166,28 @@
<value>无法连接到 LX 音乐服务器,请转到设置 - 播放源 - LX Music - LX 音乐服务器以检查是否正确输入链接</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>清理缓存...</value>
<value>正在清理缓存...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>连接失败</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>连接...</value>
<value>正在连接...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>获取文件列表...</value>
<value>正在获取文件列表...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>解析...</value>
<value>正在解析...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>正在准备清理...</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>检测到根目录路径。全盘索引可能包含大量非媒体文件并导致扫描耗时过长,建议指定具体的子目录。</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>等待扫描...</value>
<value>正在准备扫描...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>全屏模式</value>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>上次同步时间</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>本地文件夹</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>名称</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>立即同步</value>
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>未在媒体库内找到任何歌曲</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>文件夹</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>从文件导入</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>隐私政策</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>浏览</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>名称</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>留空则自动生成默认名称</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>密码</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>路径</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>找不到指定的文件夹路径</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>路径为必填项</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>端口</value>
</data>

View File

@@ -180,6 +180,12 @@
<data name="FileSystemServiceParsing" xml:space="preserve">
<value />
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value />
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value />
</data>
@@ -378,6 +384,12 @@
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value />
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value />
</data>
@@ -438,6 +450,9 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>未在媒體櫃內找到任何歌曲</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value />
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>從檔案匯入</value>
</data>
@@ -528,12 +543,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>隱私權政策</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>密碼</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>路徑</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value />
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>連接埠</value>
</data>

View File

@@ -37,33 +37,6 @@ namespace BetterLyrics.WinUI3.ViewModels
AppSettings = _settingsService.AppSettings;
}
private void AddFolderAsync(string path)
{
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (AppSettings.LocalMediaFolders.Any(x => Path.GetFullPath(x.UriPath).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
{
ToastHelper.ShowToast("SettingsPagePathExistedInfo", null, InfoBarSeverity.Warning);
}
else if (AppSettings.LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.UriPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
{
// 添加的文件夹是现有文件夹的子文件夹
ToastHelper.ShowToast("SettingsPagePathBeIncludedInfo", null, InfoBarSeverity.Warning);
}
else if (AppSettings.LocalMediaFolders.Any(item => Path.GetFullPath(item.UriPath).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
)
{
// 添加的文件夹是现有文件夹的父文件夹
ToastHelper.ShowToast("SettingsPagePathIncludingOthersInfo", null, InfoBarSeverity.Warning);
}
else
{
var tempFolder = new MediaFolder(path);
AppSettings.LocalMediaFolders.Add(tempFolder);
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(tempFolder));
}
}
public void RemoveFolder(MediaFolder folder)
{
_ = Task.Run(async () =>
@@ -84,24 +57,13 @@ namespace BetterLyrics.WinUI3.ViewModels
}
[RelayCommand]
private async Task SelectAndAddFolderAsync(UIElement sender)
{
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
if (folder != null)
{
AddFolderAsync(folder.Path);
}
}
[RelayCommand]
private async Task AddRemoteSourceAsync(string protocolType)
private async Task AddMediaSourceAsync(string protocolType)
{
var dialog = new ContentDialog
{
XamlRoot = WindowHook.GetWindow<SettingsWindow>()?.Content.XamlRoot,
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
Title = protocolType,
Title = protocolType == "Local" ? _localizationService.GetLocalizedString("MediaSettingsControlLocalFolder") : protocolType,
PrimaryButtonText = _localizationService.GetLocalizedString("Add"),
CloseButtonText = _localizationService.GetLocalizedString("Cancel"),
DefaultButton = ContentDialogButton.Primary,
@@ -111,50 +73,100 @@ namespace BetterLyrics.WinUI3.ViewModels
dialog.PrimaryButtonClick += async (s, e) =>
{
var configControl = (RemoteServerConfigControl)dialog.Content;
var deferral = e.GetDeferral();
e.Cancel = true;
e.Cancel = true; // 默认阻止关闭,直到验证通过
dialog.IsPrimaryButtonEnabled = false;
configControl.IsEnabled = false;
configControl.SetProgressBarVisibility(Visibility.Visible);
// 清除之前的错误信息
configControl.ShowError(null);
var tempFolder = configControl.GetConfig();
bool isConnected = await Task.Run(async () =>
try
{
try
var tempFolder = configControl.GetConfig();
if (protocolType == "Local")
{
using var provider = tempFolder.CreateFileSystem();
if (provider == null) return false;
string path = tempFolder.UriPath;
return await provider.ConnectAsync();
if (!System.IO.Directory.Exists(path))
{
throw new System.IO.DirectoryNotFoundException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathNotExisted"));
}
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
// 是否完全重复
if (AppSettings.LocalMediaFolders.Any(x => Path.GetFullPath(x.UriPath).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
{
configControl.ShowError(_localizationService.GetLocalizedString("SettingsPagePathExistedInfo"));
deferral.Complete();
return;
}
// 是否是子文件夹
else if (AppSettings.LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.UriPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
{
configControl.ShowError(_localizationService.GetLocalizedString("SettingsPagePathBeIncludedInfo"));
deferral.Complete();
return;
}
// 是否是父文件夹
else if (AppSettings.LocalMediaFolders.Any(item => Path.GetFullPath(item.UriPath).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase)))
{
configControl.ShowError(_localizationService.GetLocalizedString("SettingsPagePathIncludingOthersInfo"));
deferral.Complete();
return;
}
AppSettings.LocalMediaFolders.Add(tempFolder);
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(tempFolder));
e.Cancel = false; // 允许关闭
}
catch (Exception ex)
else
{
ShowErrorTip(configControl, ex.Message);
return false;
bool isConnected = await Task.Run(async () =>
{
try
{
using var provider = tempFolder.CreateFileSystem();
if (provider == null) return false;
return await provider.ConnectAsync();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
return false;
}
});
if (isConnected)
{
AppSettings.LocalMediaFolders.Add(tempFolder);
PasswordVaultHelper.Save(Constants.App.AppName, tempFolder.VaultKey, tempFolder.Password);
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(tempFolder));
e.Cancel = false; // 允许关闭
}
else
{
configControl.ShowError(_localizationService.GetLocalizedString("SettingsPageServerTestFailedInfo"));
}
}
});
if (isConnected)
{
AppSettings.LocalMediaFolders.Add(tempFolder);
PasswordVaultHelper.Save(Constants.App.AppName, tempFolder.VaultKey, tempFolder.Password);
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(tempFolder));
e.Cancel = false;
}
else
catch (Exception ex)
{
ShowErrorTip(configControl, _localizationService.GetLocalizedString("SettingsPageServerTestFailedInfo"));
configControl.ShowError(ex.Message);
}
finally
{
if (e.Cancel)
{
dialog.IsPrimaryButtonEnabled = true;
configControl.IsEnabled = true;
configControl.SetProgressBarVisibility(Visibility.Collapsed);
}
}
dialog.IsPrimaryButtonEnabled = true;
configControl.IsEnabled = true;
configControl.SetProgressBarVisibility(Visibility.Collapsed);
deferral.Complete();
};

View File

@@ -34,7 +34,8 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class MusicGalleryPageViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<DateTime?>>,
IRecipient<PropertyChangedMessage<bool>>
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>
{
private readonly ISettingsService _settingsService;
private readonly ILocalizationService _localizationService;
@@ -51,9 +52,9 @@ namespace BetterLyrics.WinUI3.ViewModels
private IUnifiedFileSystem? _currentProvider;
// All songs
private List<ExtendedTrack> _tracks = [];
// Songs in current playlist
private List<ExtendedTrack> _playlistTracks = [];
private List<ExtendedTrack> _allTracks = [];
// Songs in current playlist or songs in current file tree
private List<ExtendedTrack> _middleTracks = [];
// Filtered songs based on search query for current playlist
private List<ExtendedTrack> _filteredTracks = [];
@@ -86,13 +87,10 @@ namespace BetterLyrics.WinUI3.ViewModels
[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);
public SongsTabInfo? SelectedSongsTabInfo => AppSettings.StarredPlaylists.ElementAtOrDefault(SelectedSongsTabInfoIndex);
[ObservableProperty] public partial bool IsDataLoading { get; set; } = false;
@@ -101,6 +99,8 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial string SongSearchQuery { get; set; } = string.Empty;
public ObservableCollection<FolderNode> FolderRoots { get; } = new();
public MusicGalleryPageViewModel(
ISettingsService settingsService,
ILocalizationService localizationService,
@@ -118,8 +118,6 @@ namespace BetterLyrics.WinUI3.ViewModels
TrackPlayingQueue = [.. AppSettings.MusicGallerySettings.PlayQueuePaths.Select(x => new PlayQueueItem(new ExtendedTrack(x)))];
TrackPlayingQueue.CollectionChanged += TrackPlayingQueue_CollectionChanged;
SongsTabInfoList.Add(new SongsTabInfo(_localizationService.GetLocalizedString("MusicGalleryPageAllSongs"), "\uE8A9", false, false, CommonSongProperty.Title, string.Empty));
RefreshSongs();
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
@@ -142,7 +140,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.UriPath)];
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.DecodedAbsoluteUri)];
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
@@ -286,6 +284,7 @@ namespace BetterLyrics.WinUI3.ViewModels
.ToList();
var cachedFiles = await _fileSystemService.GetParsedFilesAsync(enabledFolderIds);
cachedFiles = cachedFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
var newTrackList = cachedFiles
.Select(x => new ExtendedTrack(x))
@@ -293,7 +292,10 @@ namespace BetterLyrics.WinUI3.ViewModels
_dispatcherQueue.TryEnqueue(() =>
{
_tracks = newTrackList;
_allTracks = newTrackList;
// 更新文件夹树
RefreshTreeView();
// 应用过滤器
ApplyPlaylist();
@@ -313,23 +315,23 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (SelectedSongsTabInfo?.FilterValue == string.Empty)
{
_playlistTracks = _tracks;
_middleTracks = _allTracks;
}
else
{
switch (SelectedSongsTabInfo?.FilterProperty)
{
case CommonSongProperty.Title:
_playlistTracks = _tracks.Where(t => t.Title.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
_middleTracks = _allTracks.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();
_middleTracks = _allTracks.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();
_middleTracks = _allTracks.Where(t => t.Artist.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
case CommonSongProperty.Folder:
_playlistTracks = _tracks.Where(t => t.ParentFolderPath.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
_middleTracks = _allTracks.Where(t => t.ParentFolderPath.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
case CommonSongProperty.M3UFilePath:
if (SelectedSongsTabInfo.FilterValue is string path)
@@ -337,11 +339,11 @@ namespace BetterLyrics.WinUI3.ViewModels
if (File.Exists(path))
{
var m3uFileContent = File.ReadAllText(path);
_playlistTracks = _tracks.Where(t => m3uFileContent.Contains(t.UriPath)).ToList();
_middleTracks = _allTracks.Where(t => m3uFileContent.Contains(t.DecodedAbsoluteUri)).ToList();
}
else
{
_playlistTracks = [];
_middleTracks = [];
ToastHelper.ShowToast("PlaylistViewFailed", path, InfoBarSeverity.Success);
}
}
@@ -359,10 +361,10 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (string.IsNullOrWhiteSpace(SongSearchQuery))
{
_filteredTracks = _playlistTracks;
_filteredTracks = _middleTracks;
return;
}
_filteredTracks = _playlistTracks.Where(t =>
_filteredTracks = _middleTracks.Where(t =>
t.Title.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Artist.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Album.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
@@ -403,17 +405,52 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
public void UpdateSelectedPlaylist(SongsTabInfo playlist)
private void RefreshTreeView()
{
var found = SongsTabInfoList.FirstOrDefault(x => x.FilterProperty == playlist.FilterProperty && x.FilterValue == playlist.FilterValue);
var roots = FolderTreeBuilder.Build(_allTracks, AppSettings.LocalMediaFolders.ToList());
FolderRoots.Clear();
foreach (var r in roots) FolderRoots.Add(r);
}
public void SelectFolder(FolderNode? folder)
{
if (folder == null) return;
if (_allTracks == null) return;
string baseUri = folder.FolderPath;
if (!baseUri.EndsWith("/")) baseUri += "/";
_middleTracks = _allTracks.Where(track =>
{
if (track.MediaFolderId != folder.MediaFolderId) return false;
string trackUriDecoded = System.Net.WebUtility.UrlDecode(track.Uri);
if (!trackUriDecoded.StartsWith(baseUri, StringComparison.OrdinalIgnoreCase)) return false;
string relativePart = trackUriDecoded.Substring(baseUri.Length);
return !relativePart.Contains('/');
}).ToList();
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
}
public void AddToPlaylists(SongsTabInfo playlist)
{
var starredPlaylists = AppSettings.StarredPlaylists;
var found = starredPlaylists.FirstOrDefault(x => x.FilterProperty == playlist.FilterProperty && x.FilterValue == playlist.FilterValue);
if (found == null)
{
SongsTabInfoList.Add(playlist);
SelectedSongsTabInfoIndex = SongsTabInfoList.Count - 1;
starredPlaylists.Add(playlist);
SelectedSongsTabInfoIndex = starredPlaylists.Count - 1;
}
else
{
SelectedSongsTabInfoIndex = SongsTabInfoList.IndexOf(found);
SelectedSongsTabInfoIndex = starredPlaylists.IndexOf(found);
}
ApplyPlaylist();
}
@@ -428,6 +465,7 @@ namespace BetterLyrics.WinUI3.ViewModels
_timelineController.Pause();
_mediaPlayer.Source = null;
// 清理旧资源
_currentStream?.Dispose();
_currentNetStream?.Dispose();
_currentStream = null;
@@ -445,50 +483,66 @@ namespace BetterLyrics.WinUI3.ViewModels
try
{
// ★ 1. 查找对应的 MediaFolder 配置
// 现在的 PlayingTrack.Uri 是标准的完整 URI (例如 smb://host/share/file.mp3)
// 我们通过对比前缀来找到它属于哪个 MediaFolder
var targetFolder = _settingsService.AppSettings.LocalMediaFolders.FirstOrDefault(f =>
PlayingTrack.Uri.StartsWith(f.GetStandardUri().AbsoluteUri, StringComparison.OrdinalIgnoreCase));
{
var fUri = f.GetStandardUri().AbsoluteUri;
return PlayingTrack.Uri.StartsWith(fUri, StringComparison.OrdinalIgnoreCase);
});
if (targetFolder == null)
{
throw new Exception($"找不到文件 {PlayingTrack.FileName} 对应的存储配置。请检查服务器设置是否已启用。");
}
// ★ 2. 创建 Provider 并连接
_currentProvider = targetFolder.CreateFileSystem();
if (_currentProvider == null) return;
await _currentProvider.ConnectAsync();
// ★ 3. 构造实体对象进行读取
// FileSystemService.OpenFileAsync 现在只需要 entity.Uri 就能工作
var fileCacheStub = new FileCacheEntity
{
Uri = PlayingTrack.Uri
};
_currentNetStream = await _fileSystemService.OpenFileAsync(_currentProvider, fileCacheStub);
var sourceStream = await _fileSystemService.OpenFileAsync(_currentProvider, fileCacheStub);
if (sourceStream == null)
{
throw new FileNotFoundException("无法打开文件流");
}
if (sourceStream.CanSeek)
{
_currentNetStream = sourceStream;
}
else
{
var memStream = new MemoryStream();
await sourceStream.CopyToAsync(memStream);
memStream.Position = 0;
sourceStream.Dispose();
_currentNetStream = memStream;
}
_currentStream = _currentNetStream.AsRandomAccessStream();
// 获取 MIME 类型 (使用 FileName 或 Uri 都可以)
string contentType = GetMimeType(PlayingTrack.FileName);
var mediaSource = MediaSource.CreateFromStream(_currentStream, contentType);
_mediaPlayer.Source = mediaSource;
// --- SMTC 更新逻辑 (基本保持不变) ---
var updater = _smtc.DisplayUpdater;
updater.Type = MediaPlaybackType.Music;
updater.MusicProperties.Title = PlayingTrack.Title ?? PlayingTrack.FileName;
updater.MusicProperties.Artist = PlayingTrack.Artist ?? "Unknown Artist";
updater.MusicProperties.Artist = PlayingTrack.Artist ?? "";
updater.MusicProperties.AlbumTitle = PlayingTrack.Album ?? "";
updater.MusicProperties.Genres.Clear();
// 注意:这里改用 FileName 获取文件名,因为 UriPath 已被移除
updater.MusicProperties.Genres.Add($"{ExtendedGenreFiled.FileName}{Path.GetFileNameWithoutExtension(PlayingTrack.FileName)}");
updater.AppMediaId = Package.Current.Id.FullName;
@@ -507,10 +561,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
catch (Exception ex)
{
// 建议:播放失败时弹个 Toast 或者在 UI 上显示错误
System.Diagnostics.Debug.WriteLine($"PlayTrackAsync Error: {ex.Message}");
// 自动跳过或停止
ToastHelper.ShowToast($"PlayTrackAsync: Error", ex.Message, InfoBarSeverity.Error);
_timelineController.Pause();
}
}
@@ -552,8 +603,6 @@ namespace BetterLyrics.WinUI3.ViewModels
FilterProperty = CommonSongProperty.M3UFilePath,
FilterValue = file.Path,
Icon = "\uE7BC",
IsStarred = true,
IsClosable = true,
Name = file.Name
});
}
@@ -619,5 +668,16 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is MediaFolder)
{
if (message.PropertyName == nameof(MediaFolder.Name))
{
RefreshTreeView();
}
}
}
}
}

View File

@@ -29,12 +29,169 @@
</Page.Resources>
<Grid>
<Grid Padding="12,0,12,64" ColumnSpacing="12">
<Grid Padding="12,8,12,64" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="SongViewer" Grid.Column="0">
<ScrollViewer Grid.Column="0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="MusicGalleryPagePlaylist"
Grid.Row="0"
Margin="1,4,0,6"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 播放列表 -->
<ListView
x:Name="StarredPlaylistsListView"
Grid.Row="0"
ItemsSource="{x:Bind ViewModel.AppSettings.StarredPlaylists, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.SelectedSongsTabInfoIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:SongsTabInfo">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
ColumnSpacing="6"
Tapped="PlaylistGrid_Tapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="{x:Bind Icon}" />
<TextBlock
Grid.Column="1"
Margin="0,8"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind Name}"
TextWrapping="Wrap" />
</Grid>
<!-- 从播放列表移除 -->
<Button
Grid.Column="1"
Click="RemoveFromPlaylistButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE711;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind IsDefault, Converter={StaticResource BoolNegationToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromCustomList" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<NavigationViewItemSeparator Grid.Row="1" />
<!-- 命令区域 -->
<Grid Grid.Row="2">
<StackPanel>
<!-- 创建播放列表 -->
<Button
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
Command="{x:Bind ViewModel.CreatePlaylistCommand}">
<Button.Content>
<StackPanel
Padding="4,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE710;" />
<TextBlock x:Uid="MusicGalleryPageNewPlaylist" />
</StackPanel>
</Button.Content>
</Button>
<!-- 导入播放列表 -->
<Button
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
Command="{x:Bind ViewModel.ImportPlaylistCommand}">
<Button.Content>
<StackPanel
Padding="4,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE896;" />
<TextBlock x:Uid="MusicGalleryPageImportFromFile" />
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</Grid>
</Grid>
<NavigationViewItemSeparator Grid.Row="2" />
<TextBlock
x:Uid="MusicGalleryPageFolder"
Grid.Row="3"
Margin="1,4,0,6"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<TreeView
x:Name="FolderTreeView"
Grid.Row="4"
AllowDrop="False"
CanDragItems="False"
ItemInvoked="FolderTreeView_ItemInvoked"
ItemsSource="{x:Bind ViewModel.FolderRoots, Mode=OneWay}"
SelectionMode="Single">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="models:FolderNode">
<TreeViewItem IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}" ItemsSource="{x:Bind SubFolders}">
<StackPanel Orientation="Horizontal" Spacing="10">
<FontIcon FontSize="16" Glyph="{x:Bind SourceType, Converter={StaticResource FileSourceTypeToIconConverter}}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind FolderName}" />
</StackPanel>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</ScrollViewer>
<Grid x:Name="SongViewer" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -78,10 +235,7 @@
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoBitDepth" Value="{x:Bind ViewModel.TrackRightTapped.BitDepth, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoFormat" Value="{x:Bind ViewModel.TrackRightTapped.AudioFormatName, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoEncoder" Value="{x:Bind ViewModel.TrackRightTapped.Encoder, Mode=OneWay}" />
<uc:PropertyRow
x:Uid="MusicGalleryPageFileInfoPath"
Link="{x:Bind ViewModel.TrackRightTapped.Uri, Mode=OneWay}"
Value="{x:Bind ViewModel.TrackRightTapped.Uri, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoPath" Value="{x:Bind ViewModel.TrackRightTapped.DecodedAbsoluteUri, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoLyrics" Value="{x:Bind ViewModel.TrackRightTapped.RawLyrics, Mode=OneWay}" />
</StackPanel>
</Grid>
@@ -91,199 +245,9 @@
<StackPanel Grid.Row="0" Spacing="6">
<Grid VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
x:Name="PlaylistButton"
Grid.Column="0"
Style="{StaticResource GhostButtonStyle}">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE728;" />
<TextBlock x:Uid="MusicGalleryPagePlaylist" />
</StackPanel>
</Button.Content>
<Button.Flyout>
<Flyout FlyoutPresenterStyle="{StaticResource FlyoutGhostStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button
Grid.Row="0"
Margin="4"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
Command="{x:Bind ViewModel.CreatePlaylistCommand}">
<Button.Content>
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE710;" />
<TextBlock x:Uid="MusicGalleryPageNewPlaylist" />
</StackPanel>
</Button.Content>
</Button>
<Button
Grid.Row="1"
Margin="4,1,4,2"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
Command="{x:Bind ViewModel.ImportPlaylistCommand}">
<Button.Content>
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE74B;" />
<TextBlock x:Uid="MusicGalleryPageImportFromFile" />
</StackPanel>
</Button.Content>
</Button>
<ListView
x:Name="StarredPlaylistsListView"
Grid.Row="2"
ItemsSource="{x:Bind ViewModel.AppSettings.StarredPlaylists, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:SongsTabInfo">
<Grid Tapped="StarredPlaylistsListViewItemGrid_Tapped">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="{x:Bind Icon}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind Name}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Flyout>
</Button.Flyout>
</Button>
<ListView
Grid.Column="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.SongsTabInfoList, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollMode="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 Padding="12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="0"
Click="PlaylistFavButton_Click"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind IsClosable, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ToolTipService.ToolTip>
<Grid>
<TextBlock x:Uid="MusicGalleryPageAddToCustomList" Visibility="{x:Bind IsStarred, Converter={StaticResource BoolNegationToVisibilityConverter}, Mode=OneWay}" />
<TextBlock x:Uid="MusicGalleryPageRemoveFromCustomList" Visibility="{x:Bind IsStarred, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</Grid>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE734;"
Opacity="{x:Bind IsStarred, Converter={StaticResource BoolNegationToOpacityConverter}, Mode=OneWay}">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE735;"
Opacity="{x:Bind IsStarred, Converter={StaticResource BoolToOpacityConverter}, Mode=OneWay}">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<Grid
Grid.Column="1"
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="{x:Bind Icon}" />
<TextBlock
Grid.Column="1"
Margin="0,0,0,2"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind Name}" />
</Grid>
<Button
Grid.Column="2"
Click="PlaylistCloseButton_Click"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE711;}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind IsClosable, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<AutoSuggestBox
x:Name="SongSearchBox"
x:Uid="MusicGalleryPageSongSearchBox"
Margin="0,-8,0,0"
HorizontalAlignment="Stretch"
QueryIcon="Find"
Text="{x:Bind ViewModel.SongSearchQuery, Mode=TwoWay}" />
@@ -388,70 +352,53 @@
DoubleTapped="SongListViewItem_DoubleTapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
MaxWidth="48"
MaxHeight="48"
CornerRadius="4">
<Image Source="{x:Bind LocalAlbumArtPath, Mode=OneWay, Converter={StaticResource PathToImageConverter}}" Stretch="Uniform" />
<!-- 标题 -->
<Grid Grid.Column="0">
<TextBlock
VerticalAlignment="Center"
Text="{x:Bind Title}"
TextWrapping="Wrap" />
</Grid>
<!-- 基本信息 -->
<!-- 艺术家 -->
<Grid Grid.Column="1">
<StackPanel VerticalAlignment="Center" Spacing="6">
<TextBlock Text="{x:Bind Title}" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="6">
<Grid Background="{ThemeResource AccentAcrylicBackgroundFillColorBaseBrush}" CornerRadius="4">
<TextBlock
Margin="4,2"
FontSize="12"
Text="{x:Bind AudioFormatShortName}" />
</Grid>
<HyperlinkButton Padding="0" Click="ArtistHyperlibkButton_Click">
<TextBlock Text="{x:Bind Artist}" TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
</StackPanel>
<HyperlinkButton Click="ArtistHyperlibkButton_Click">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageAddToCustomList" />
</ToolTipService.ToolTip>
<TextBlock Text="{x:Bind Artist}" TextWrapping="Wrap" />
</HyperlinkButton>
</Grid>
<HyperlinkButton Grid.Column="2" Click="AlbumHyperlibkButton_Click">
<TextBlock Text="{x:Bind Album}" TextWrapping="Wrap" />
</HyperlinkButton>
<!-- 年份 -->
<TextBlock
Grid.Column="3"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Year}"
TextWrapping="Wrap" />
<!-- 专辑 -->
<Grid Grid.Column="2">
<HyperlinkButton Click="AlbumHyperlibkButton_Click">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageAddToCustomList" />
</ToolTipService.ToolTip>
<TextBlock Text="{x:Bind Album}" TextWrapping="Wrap" />
</HyperlinkButton>
</Grid>
<!-- 歌曲时长 -->
<TextBlock
Grid.Column="4"
Grid.Column="3"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}}"
TextWrapping="Wrap" />
<!-- 路径 -->
<HyperlinkButton
Grid.Column="5"
VerticalAlignment="Center"
Click="PathHyperlibkButton_Click"
Content="{x:Bind ParentFolderName}" />
<!-- 更多 -->
<Button
Grid.Column="6"
Grid.Column="4"
HorizontalAlignment="Right"
Click="SongListViewItemMoreButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
@@ -515,136 +462,134 @@
</Grid>
<Grid x:Name="PlayQueue" Grid.Column="1">
<Grid x:Name="PlayQueue" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Spacing="6">
<Grid ColumnSpacing="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
Grid.Column="0"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<StackPanel
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<Grid Grid.Row="0">
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
</Grid>
<!-- Stop media session -->
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Playback order -->
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.SwitchPlaybackOrderCommand}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<!-- Stop media session -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Playback order -->
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.SwitchPlaybackOrderCommand}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</Button.Content>
</Button>
<!-- Scroll to playing item -->
<Button
Grid.Column="4"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="5"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</StackPanel>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<!-- Scroll to playing item -->
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="4"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<ListView
x:Name="PlayingQueueListView"
Grid.Row="1"
Grid.Row="3"
ItemsSource="{x:Bind ViewModel.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
@@ -676,7 +621,7 @@
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="1">
<Grid Grid.Row="3">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
@@ -703,6 +648,7 @@
</StackPanel>
</Grid>
</Grid>
</Grid>
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}" Visibility="{x:Bind ViewModel.IsDataLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">

View File

@@ -51,7 +51,7 @@ namespace BetterLyrics.WinUI3.Views
private async void SongPathHyperlinkButton_Click(object sender, RoutedEventArgs e)
{
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).UriPath);
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).DecodedAbsoluteUri);
}
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
@@ -140,36 +140,47 @@ namespace BetterLyrics.WinUI3.Views
private void ArtistHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var artist = ((ExtendedTrack)((FrameworkElement)sender).DataContext).Artist;
var playlist = new SongsTabInfo(artist, "\uEFA9", true, false, CommonSongProperty.Artist, artist);
ViewModel.UpdateSelectedPlaylist(playlist);
var playlist = new SongsTabInfo
{
Name = artist,
Icon = "\uEFA9",
FilterProperty = CommonSongProperty.Artist,
FilterValue = artist
};
ViewModel.AddToPlaylists(playlist);
}
private void AlbumHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var album = ((ExtendedTrack)((FrameworkElement)sender).DataContext).Album;
var playlist = new SongsTabInfo(album, "\uE93C", true, false, CommonSongProperty.Album, album);
ViewModel.UpdateSelectedPlaylist(playlist);
var playlist = new SongsTabInfo
{
Name = album,
Icon = "\uE93C",
FilterProperty = CommonSongProperty.Album,
FilterValue = album
};
ViewModel.AddToPlaylists(playlist);
}
private void PathHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var track = ((ExtendedTrack)((FrameworkElement)sender).DataContext);
var playlist = new SongsTabInfo(track.ParentFolderName, "\uE8B7", true, false, CommonSongProperty.Folder, track.ParentFolderPath);
ViewModel.UpdateSelectedPlaylist(playlist);
var playlist = new SongsTabInfo
{
Name = track.ParentFolderName,
Icon = "\uE8B7",
FilterProperty = CommonSongProperty.Folder,
FilterValue = track.ParentFolderPath
};
ViewModel.AddToPlaylists(playlist);
}
private void PlaylistGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
FolderTreeView.SelectedItem = null;
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();
ViewModel.AddToPlaylists(playlist);
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
@@ -181,28 +192,12 @@ namespace BetterLyrics.WinUI3.Views
}
}
private void PlaylistFavButton_Click(object sender, RoutedEventArgs e)
private void RemoveFromPlaylistButton_Click(object sender, RoutedEventArgs e)
{
var playlist = (SongsTabInfo)((FrameworkElement)sender).DataContext;
var targetStatus = !playlist.IsStarred;
if (targetStatus)
{
ViewModel.AppSettings.StarredPlaylists.Add(playlist);
}
else
{
ViewModel.AppSettings.StarredPlaylists.Remove(playlist);
}
playlist.IsStarred = targetStatus;
}
private void StarredPlaylistsListViewItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var songsTabInfo = ((SongsTabInfo)((FrameworkElement)sender).DataContext);
if (!ViewModel.SongsTabInfoList.Contains(songsTabInfo))
{
ViewModel.SongsTabInfoList.Add(songsTabInfo);
}
ViewModel.AppSettings.StarredPlaylists.Remove(playlist);
ViewModel.SelectedSongsTabInfoIndex = 0;
ViewModel.ApplyPlaylist();
}
private void SongListViewItemMoreButton_Click(object sender, RoutedEventArgs e)
@@ -223,7 +218,7 @@ namespace BetterLyrics.WinUI3.Views
private void AddToPlaylistMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
((MenuFlyoutItem)sender).ContextFlyout.ShowAt(PlaylistButton);
//((MenuFlyoutItem)sender).ContextFlyout.ShowAt(PlaylistButton);
}
private void ToBeAddedPlaylistsListViewItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
@@ -236,7 +231,7 @@ namespace BetterLyrics.WinUI3.Views
if (File.Exists(path))
{
var content = File.ReadAllText(path);
foreach (var item in ViewModel.SelectedTracks.Select(x => x.UriPath).ToList())
foreach (var item in ViewModel.SelectedTracks.Select(x => x.DecodedAbsoluteUri).ToList())
{
if (!content.Contains(item))
{
@@ -279,5 +274,13 @@ namespace BetterLyrics.WinUI3.Views
ScrollToPlayingItem();
}
private void FolderTreeView_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
{
ViewModel.SelectedSongsTabInfoIndex = -1;
if (args.InvokedItem is FolderNode selectedFolder)
{
ViewModel.SelectFolder(selectedFolder);
}
}
}
}

View File

@@ -16,7 +16,7 @@
Loaded="RootGrid_Loaded"
Unloaded="RootGrid_Unloaded">
<local:MusicGalleryPage x:Name="MusicGalleryPage" Margin="0,40,0,0" />
<local:MusicGalleryPage x:Name="MusicGalleryPage" />
<local:NowPlayingPage
x:Name="NowPlayingPage"

View File

@@ -227,8 +227,8 @@
<Flyout FlyoutPresenterStyle="{StaticResource FlyoutGhostStyle}" ShouldConstrainToRootBounds="False">
<Grid
x:Name="TopCommandFlyoutContainer"
Width="400"
Height="36" />
Width="450"
Height="38" />
</Flyout>
</Button.Flyout>
</Button>

View File

@@ -11,9 +11,13 @@
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Frame x:Name="RootFrame" Margin="0,40,0,0" />
<Frame x:Name="RootFrame" />
<StackPanel VerticalAlignment="Top" Orientation="Horizontal">
<StackPanel
Margin="0,0,140,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal">
<Button Click="LyricsWindowSwitchButton_Click" Style="{StaticResource TitleBarButtonStyle}">
<FontIcon