mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-13 03:34:55 +08:00
fix: lyrics source search issue when id is not null; feat: support ftp, smb, webdav (still testing)
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="37412.BetterLyrics"
|
||||
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
|
||||
Version="1.1.198.0" />
|
||||
Version="1.1.201.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
<converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
|
||||
<converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
|
||||
<converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
|
||||
<converter:TrackToLyricsConverter x:Key="TrackToLyricsConverter" />
|
||||
<converter:IntToBoolConverter x:Key="IntToBoolConverter" />
|
||||
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
|
||||
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
|
||||
@@ -75,6 +74,7 @@
|
||||
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
|
||||
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
|
||||
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
|
||||
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<None Remove="Controls\NowPlayingBar.xaml" />
|
||||
<None Remove="Controls\PlaybackSettingsControl.xaml" />
|
||||
<None Remove="Controls\PropertyRow.xaml" />
|
||||
<None Remove="Controls\RemoteServerConfigControl.xaml" />
|
||||
<None Remove="Controls\ShortcutTextBox.xaml" />
|
||||
<None Remove="Controls\SystemTray.xaml" />
|
||||
<None Remove="Controls\WindowSettingsControl.xaml" />
|
||||
@@ -58,20 +59,21 @@
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251021-build.2365" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.251219" />
|
||||
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
|
||||
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
|
||||
<PackageReference Include="DevWinUI.Controls" Version="9.8.1" />
|
||||
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" />
|
||||
<PackageReference Include="F23.StringSimilarity" Version="7.0.1" />
|
||||
<PackageReference Include="FlaUI.UIA3" Version="5.0.0" />
|
||||
<PackageReference Include="FluentFTP" Version="53.0.2" />
|
||||
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.4.1" />
|
||||
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
|
||||
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
|
||||
@@ -85,6 +87,7 @@
|
||||
<PackageReference Include="NTextCat" Version="0.3.65" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SMBLibrary" Version="1.5.5.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.1" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
@@ -95,6 +98,7 @@
|
||||
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" />
|
||||
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
|
||||
<PackageReference Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="7.9.0" />
|
||||
</ItemGroup>
|
||||
@@ -339,6 +343,11 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="TemplateSelector\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\RemoteServerConfigControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\NowPlayingBar.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="BetterLyrics.WinUI3.Controls.MediaSettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
@@ -9,6 +8,7 @@
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:BetterLyrics.WinUI3.Models"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -49,25 +49,32 @@
|
||||
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<dev:SettingsExpander>
|
||||
<DataTemplate x:DataType="models:MediaFolder">
|
||||
<dev:SettingsExpander Description="{x:Bind ConnectionSummary, Mode=OneWay}" HeaderIcon="{Binding SourceType, Converter={StaticResource FileSourceTypeToIconConverter}}">
|
||||
|
||||
<dev:SettingsExpander.Header>
|
||||
<HyperlinkButton
|
||||
Padding="0"
|
||||
Click="LocalFolderHyperlinkButton_Click"
|
||||
Content="{Binding Path, Mode=OneWay}"
|
||||
Tag="{Binding Path, Mode=OneWay}" />
|
||||
Content="{x:Bind Path, Mode=OneWay}"
|
||||
Tag="{x:Bind Path, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ConnectionSummary}" />
|
||||
</dev:SettingsExpander.Header>
|
||||
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
|
||||
<dev:SettingsExpander.Items>
|
||||
<dev:SettingsCard>
|
||||
<dev:SettingsCard.Header>
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsPageRemovePath"
|
||||
Padding="0"
|
||||
Click="SettingsPageRemovePathButton_Click"
|
||||
Tag="{Binding}" />
|
||||
</dev:SettingsCard.Header>
|
||||
</dev:SettingsCard>
|
||||
<dev:SettingsCard x:Uid="SettingsPageMusicLibRealTimeWatch">
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageMusicLibRealTimeWatch" IsEnabled="{Binding IsLocal, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{Binding IsRealTimeWatchEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
</dev:SettingsExpander.Items>
|
||||
@@ -76,15 +83,55 @@
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
|
||||
<Button
|
||||
x:Uid="SettingsPageAddFolderButton"
|
||||
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
|
||||
CommandParameter="{Binding ElementName=RootGrid}" />
|
||||
<dev:SettingsCard Style="{StaticResource DefaultSettingsExpanderItemStyle}">
|
||||
<DropDownButton x:Uid="SettingsPageAddFolderButton">
|
||||
<DropDownButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="SettingsPageLocalFolder"
|
||||
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
|
||||
CommandParameter="{Binding ElementName=RootGrid}"
|
||||
Icon="Folder" />
|
||||
|
||||
<MenuFlyoutSeparator />
|
||||
|
||||
<MenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
|
||||
CommandParameter="SMB"
|
||||
Text="SMB"
|
||||
Visibility="Collapsed">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
|
||||
CommandParameter="FTP"
|
||||
Text="FTP Server"
|
||||
Visibility="Collapsed">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
|
||||
CommandParameter="WebDAV"
|
||||
Text="WebDAV"
|
||||
Visibility="Collapsed">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
</MenuFlyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
</dev:SettingsCard>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
@@ -22,7 +22,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.RemoveFolderAsync((LocalMediaFolder)(sender as HyperlinkButton)!.Tag);
|
||||
ViewModel.RemoveFolderAsync((MediaFolder)(sender as HyperlinkButton)!.Tag);
|
||||
}
|
||||
|
||||
private async void LocalFolderHyperlinkButton_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="BetterLyrics.WinUI3.Controls.RemoteServerConfigControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<StackPanel Width="400" Spacing="16">
|
||||
<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"
|
||||
Header="Server Address"
|
||||
InputScope="Url"
|
||||
PlaceholderText="192.168.1.x"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<NumberBox
|
||||
x:Name="PortBox"
|
||||
x:Uid="RemoteServerConfigControlPort"
|
||||
Grid.Column="1"
|
||||
MinWidth="100"
|
||||
Header="Port"
|
||||
LargeChange="10"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
ToolTipService.ToolTip="80"
|
||||
Value="80" />
|
||||
</Grid>
|
||||
|
||||
<TextBox
|
||||
x:Name="PathBox"
|
||||
x:Uid="RemoteServerConfigControlPath"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<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>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,101 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using DevWinUI;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
public sealed partial class RemoteServerConfigControl : UserControl
|
||||
{
|
||||
private readonly string _protocolType;
|
||||
|
||||
public RemoteServerConfigControl(string protocolType)
|
||||
{
|
||||
this.InitializeComponent();
|
||||
_protocolType = protocolType;
|
||||
|
||||
SetupDefaults();
|
||||
}
|
||||
|
||||
private void SetupDefaults()
|
||||
{
|
||||
switch (_protocolType.ToUpper())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public MediaFolder GetConfig()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(HostBox.Text))
|
||||
throw new ArgumentException("Server address is required.");
|
||||
|
||||
string name = $"{_protocolType} - {HostBox.Text}";
|
||||
|
||||
Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
|
||||
|
||||
var folder = new MediaFolder
|
||||
{
|
||||
Name = name,
|
||||
Path = HostBox.Text, // <20><><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP/Host
|
||||
Port = (int)PortBox.Value,
|
||||
UserName = UserBox.Text,
|
||||
Password = PwdBox.Password, // <20><> PasswordBox <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>
|
||||
SourceType = sourceType,
|
||||
IsRealTimeWatchEnabled = false
|
||||
};
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><E2B4A6>·<EFBFBD><C2B7><EFBFBD><EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA>"Զ<><D4B6>·<EFBFBD><C2B7>"ƴ<>ӵ<EFBFBD> Path <20><EFBFBD><EFA3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>ֶδ<D6B6>
|
||||
// Ϊ<>˼<CBBC><F2B5A5A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѭ<EFBFBD><D1AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD>壺
|
||||
// <20><><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD><EFBFBD><EFBFBD>ټ<EFBFBD>һ<EFBFBD><D2BB> RemotePath <20>ֶΣ<D6B6><CEA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
|
||||
// *<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD>壬<EFBFBD><E5A3AC><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD>
|
||||
// Path <20>ֶδ洢<CEB4><E6B4A2>ʽ<EFBFBD><CABD> "192.168.1.5/Music"
|
||||
|
||||
var rawPath = PathBox.Text.Trim().TrimStart('/', '\\'); // ȥ<><C8A5><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7>б<EFBFBD><D0B1>
|
||||
if (!string.IsNullOrEmpty(rawPath))
|
||||
{
|
||||
// <20><EFBFBD>·<EFBFBD><C2B7>ƴ<EFBFBD><C6B4><EFBFBD><EFBFBD>
|
||||
if (sourceType == FileSourceType.SMB)
|
||||
{
|
||||
// SMBLibrary <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD> Host <20>ֿ<EFBFBD><D6BF><EFBFBD>ShareName <20>ֿ<EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP <20><><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFA3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA> ShareName ƴ<>ں<EFBFBD><DABA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֶ<EFBFBD>
|
||||
// Ϊ<>˷<EFBFBD><CBB7>㣬<EFBFBD><E3A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP <20><> ShareName ƴ<><C6B4>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Path
|
||||
// <20><><EFBFBD><EFBFBD>: 192.168.1.5/Music
|
||||
folder.Path = $"{HostBox.Text}/{rawPath}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// FTP/WebDAV: 192.168.1.5/pub/music
|
||||
folder.Path = $"{HostBox.Text}/{rawPath}";
|
||||
}
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
public void ShowError(string message)
|
||||
{
|
||||
ErrorInfoBar.Message = message;
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class FileSourceTypeToIconConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is FileSourceType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
FileSourceType.Local => new FontIcon { Glyph = "\uE8B7" }, // Folder
|
||||
FileSourceType.SMB => new FontIcon { Glyph = "\uE839" }, // Network
|
||||
FileSourceType.FTP => new FontIcon { Glyph = "\uE838" }, // Globe
|
||||
FileSourceType.WebDav => new FontIcon { Glyph = "\uE753" }, // Cloud
|
||||
_ => new FontIcon { Glyph = "\uE8B7" }
|
||||
};
|
||||
}
|
||||
return new FontIcon { Glyph = "\uE8B7" };
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class TrackToLyricsConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Track track)
|
||||
{
|
||||
return track.GetRawLyrics();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
{
|
||||
public enum FileSourceType
|
||||
{
|
||||
Local,
|
||||
SMB,
|
||||
FTP,
|
||||
WebDav
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using ATL;
|
||||
using System.IO;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class TrackExtensions
|
||||
{
|
||||
extension(Track track)
|
||||
{
|
||||
public string GetParentFolderName() => Directory.GetParent(track.Path)?.Name ?? "";
|
||||
|
||||
public string GetParentFolderPath() => Directory.GetParent(track.Path)?.FullName ?? "";
|
||||
|
||||
public string GetRawLyrics()
|
||||
{
|
||||
if (track.Path is string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return TagLib.File.Create(path).Tag.Lyrics;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public string GetFileName() => Path.GetFileName(track.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using BetterLyrics.WinUI3.Models.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class ExtendedTrack : ATL.Track
|
||||
{
|
||||
public new string Path { get; private set; } = "";
|
||||
public string RawLyrics { get; set; } = "";
|
||||
public string ParentFolderName => Directory.GetParent(Path)?.Name ?? "";
|
||||
public string ParentFolderPath => Directory.GetParent(Path)?.FullName ?? "";
|
||||
public string FileName => System.IO.Path.GetFileName(Path);
|
||||
|
||||
public ExtendedTrack() : base() { }
|
||||
|
||||
public ExtendedTrack(string path) : base(path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public ExtendedTrack(string path, Stream stream) : base(stream, System.IO.Path.GetExtension(path))
|
||||
{
|
||||
Path = path;
|
||||
SetRawLyrics(new StreamFileAbstraction(path, stream));
|
||||
}
|
||||
|
||||
private void SetRawLyrics(StreamFileAbstraction streamFileAbstraction)
|
||||
{
|
||||
try
|
||||
{
|
||||
RawLyrics = TagLib.File.Create(streamFileAbstraction).Tag.Lyrics;
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using FluentFTP;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public partial class FTPFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private readonly AsyncFtpClient _client;
|
||||
private readonly string _rootPath; // 服务器上的根路径 (例如 /pub/music)
|
||||
|
||||
public FTPFileSystem(string host, string user, string pass, int port, string remotePath)
|
||||
{
|
||||
// 如果 path 是 "192.168.1.5/Music",我们需要把 /Music 拆出来
|
||||
// 但为了简单,假设 host 仅仅是 IP,remotePath 才是路径
|
||||
_rootPath = remotePath ?? "/";
|
||||
|
||||
var config = new FtpConfig { ConnectTimeout = 5000 };
|
||||
_client = new AsyncFtpClient(host, user ?? "anonymous", pass ?? "", port > 0 ? port : 21, config);
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
await _client.AutoConnect();
|
||||
return _client.IsConnected;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
{
|
||||
string targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
|
||||
|
||||
var items = await _client.GetListing(targetPath);
|
||||
return items.Select(i => new UnifiedFileItem
|
||||
{
|
||||
Name = i.Name,
|
||||
FullPath = i.FullName,
|
||||
IsFolder = i.Type == FtpObjectType.Directory,
|
||||
Size = i.Size,
|
||||
LastModified = i.Modified
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
{
|
||||
return await _client.OpenRead(fullPath);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync() => await _client.Disconnect();
|
||||
public void Dispose() => _client?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public interface IUnifiedFileSystem : IDisposable
|
||||
{
|
||||
Task<bool> ConnectAsync();
|
||||
Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath);
|
||||
Task<Stream> OpenReadAsync(string fullPath);
|
||||
Task DisconnectAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public partial class LocalFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
|
||||
public LocalFileSystem(string rootPath)
|
||||
{
|
||||
_rootPath = rootPath;
|
||||
}
|
||||
|
||||
public Task<bool> ConnectAsync()
|
||||
{
|
||||
return Task.FromResult(Directory.Exists(_rootPath));
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
{
|
||||
var result = new List<UnifiedFileItem>();
|
||||
|
||||
var targetPath = string.IsNullOrWhiteSpace(relativePath)
|
||||
? _rootPath
|
||||
: Path.Combine(_rootPath, relativePath);
|
||||
|
||||
if (!Directory.Exists(targetPath)) return result;
|
||||
|
||||
var dirInfo = new DirectoryInfo(targetPath);
|
||||
|
||||
foreach (var item in dirInfo.GetFileSystemInfos())
|
||||
{
|
||||
bool isDir = (item.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
result.Add(new UnifiedFileItem
|
||||
{
|
||||
Name = item.Name,
|
||||
FullPath = item.FullName,
|
||||
IsFolder = isDir,
|
||||
Size = isDir ? 0 : ((FileInfo)item).Length,
|
||||
LastModified = item.LastWriteTime
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
{
|
||||
return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync() => await Task.CompletedTask;
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using SMBLibrary;
|
||||
using SMBLibrary.Client;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public partial class SMBFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private SMB2Client _client;
|
||||
private ISMBFileStore _fileStore;
|
||||
|
||||
private readonly string _ip;
|
||||
private readonly string _shareName;
|
||||
private readonly string _pathInsideShare; // 共享里的子路径
|
||||
private readonly string _username;
|
||||
private readonly string _password;
|
||||
|
||||
// fullPathInput 例如: "192.168.1.5/Music/Pop"
|
||||
public SMBFileSystem(string fullPathInput, string user, string pass)
|
||||
{
|
||||
_username = user;
|
||||
_password = pass;
|
||||
|
||||
// 解析路径:分离 IP 和 共享名
|
||||
var parts = fullPathInput.Replace("\\", "/").Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length >= 1) _ip = parts[0];
|
||||
if (parts.Length >= 2) _shareName = parts[1];
|
||||
|
||||
// 剩下的部分重新拼起来作为子路径
|
||||
if (parts.Length > 2)
|
||||
_pathInsideShare = string.Join("\\", parts.Skip(2));
|
||||
else
|
||||
_pathInsideShare = "";
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
_client = new SMB2Client();
|
||||
bool connected = _client.Connect(_ip, SMBTransportType.DirectTCPTransport);
|
||||
if (!connected) return false;
|
||||
|
||||
var status = _client.Login(string.Empty, _username, _password);
|
||||
if (status != NTStatus.STATUS_SUCCESS) return false;
|
||||
|
||||
// 连接具体的共享文件夹
|
||||
if (string.IsNullOrEmpty(_shareName)) return true; // 只连了服务器,没连共享
|
||||
|
||||
_fileStore = _client.TreeConnect(_shareName, out status);
|
||||
return status == NTStatus.STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
{
|
||||
var result = new List<UnifiedFileItem>();
|
||||
if (_fileStore == null) return result;
|
||||
|
||||
// 拼接完整路径: Root里面的子路径 + 传入的相对路径
|
||||
string queryPath = Path.Combine(_pathInsideShare, relativePath).Replace("/", "\\").TrimStart('\\');
|
||||
|
||||
// 打开目录
|
||||
var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, queryPath,
|
||||
AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read,
|
||||
CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
|
||||
|
||||
if (statusRet != NTStatus.STATUS_SUCCESS) return result;
|
||||
|
||||
List<QueryDirectoryFileInformation> fileInfo;
|
||||
do
|
||||
{
|
||||
statusRet = _fileStore.QueryDirectory(out fileInfo, handle, "*", FileInformationClass.FileDirectoryInformation);
|
||||
|
||||
List<FileDirectoryInformation> list = fileInfo.Select(x => (FileDirectoryInformation)x).ToList();
|
||||
foreach (var item in list)
|
||||
{
|
||||
// 排除当前目录和父目录
|
||||
if (item.FileName == "." || item.FileName == "..") continue;
|
||||
|
||||
result.Add(new UnifiedFileItem
|
||||
{
|
||||
Name = item.FileName,
|
||||
FullPath = Path.Combine(queryPath, item.FileName),
|
||||
IsFolder = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
|
||||
Size = item.AllocationSize,
|
||||
LastModified = item.LastWriteTime
|
||||
});
|
||||
}
|
||||
|
||||
if (statusRet == NTStatus.STATUS_NO_MORE_FILES)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (statusRet != NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
// Log
|
||||
break;
|
||||
}
|
||||
} while (statusRet == NTStatus.STATUS_SUCCESS);
|
||||
|
||||
_fileStore.CloseFile(handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
{
|
||||
var ret = _fileStore.CreateFile(out object handle, out FileStatus status, fullPath,
|
||||
AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, 0, null);
|
||||
|
||||
if (ret != NTStatus.STATUS_SUCCESS) throw new IOException($"SMB Open Error: {ret}");
|
||||
|
||||
return new SMBReadOnlyStream(_fileStore, handle);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
_client?.Disconnect();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client?.Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using SMBLibrary;
|
||||
using SMBLibrary.Client;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public partial class SMBReadOnlyStream : Stream
|
||||
{
|
||||
private readonly ISMBFileStore _store;
|
||||
private readonly object _handle;
|
||||
private long _position;
|
||||
private long _length; // 新增:缓存文件长度
|
||||
|
||||
public SMBReadOnlyStream(ISMBFileStore store, object handle)
|
||||
{
|
||||
_store = store;
|
||||
_handle = handle;
|
||||
_position = 0;
|
||||
|
||||
var status = _store.GetFileInformation(out FileInformation result, handle, FileInformationClass.FileStandardInformation);
|
||||
if (status == NTStatus.STATUS_SUCCESS && result is FileStandardInformation info)
|
||||
{
|
||||
_length = info.EndOfFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果获取失败,这是一个严重问题,意味着无法 Seek 到末尾
|
||||
// 暂时设为 0,但后续读取可能会出问题
|
||||
_length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => _length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
set => _position = value;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// 保护:如果位置已经超过文件末尾,直接返回 0 (EOF)
|
||||
if (_position >= _length) return 0;
|
||||
|
||||
// 保护:防止读取越界 (请求读取量不能超过剩余量)
|
||||
long remaining = _length - _position;
|
||||
int bytesToRequest = (int)Math.Min(count, remaining);
|
||||
|
||||
// 为了安全,保留对 remaining 的检查是必须的
|
||||
if (bytesToRequest <= 0) return 0;
|
||||
|
||||
var status = _store.ReadFile(out byte[] data, _handle, _position, bytesToRequest);
|
||||
|
||||
if (status == NTStatus.STATUS_END_OF_FILE) return 0;
|
||||
|
||||
if (status != NTStatus.STATUS_SUCCESS)
|
||||
{
|
||||
throw new IOException($"SMB Read failed. Status: {status} (Pos: {_position}, Req: {bytesToRequest})");
|
||||
}
|
||||
|
||||
if (data == null || data.Length == 0) return 0;
|
||||
|
||||
Array.Copy(data, 0, buffer, offset, data.Length);
|
||||
_position += data.Length;
|
||||
return data.Length;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPos = _position;
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin:
|
||||
newPos = offset;
|
||||
break;
|
||||
case SeekOrigin.Current:
|
||||
newPos = _position + offset;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
newPos = _length + offset;
|
||||
break;
|
||||
}
|
||||
|
||||
// 允许 Seek 超过 EOF (标准 Stream 行为),但在 Read 时会返回 0
|
||||
if (newPos < 0)
|
||||
{
|
||||
throw new IOException("An attempt was made to move the file pointer before the beginning of the file.");
|
||||
}
|
||||
|
||||
_position = newPos;
|
||||
return _position;
|
||||
}
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
public override void Flush() { }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
try { _store.CloseFile(_handle); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public class StreamFileAbstraction : TagLib.File.IFileAbstraction
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly Stream _stream;
|
||||
private readonly bool _closeStreamOnDispose;
|
||||
|
||||
public StreamFileAbstraction(string path, Stream stream, bool closeStreamOnDispose = false)
|
||||
{
|
||||
_name = Path.GetFileName(path);
|
||||
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
_closeStreamOnDispose = closeStreamOnDispose;
|
||||
}
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public Stream ReadStream => _stream;
|
||||
|
||||
public Stream WriteStream
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stream.CanWrite)
|
||||
{
|
||||
return _stream;
|
||||
}
|
||||
throw new InvalidOperationException("The underlying stream is read-only. Tag saving is not supported for this source.");
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseStream(Stream stream)
|
||||
{
|
||||
if (_closeStreamOnDispose)
|
||||
{
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public class UnifiedFileItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FullPath { get; set; }
|
||||
public long Size { get; set; }
|
||||
public bool IsFolder { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WebDav;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.FileSystem
|
||||
{
|
||||
public partial class WebDavFileSystem : IUnifiedFileSystem
|
||||
{
|
||||
private readonly WebDavClient _client;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _rootPath;
|
||||
|
||||
// host: http://192.168.1.5:5005
|
||||
// path: /music
|
||||
public WebDavFileSystem(string host, string user, string pass, int port, string path)
|
||||
{
|
||||
if (!host.StartsWith("http")) host = $"http://{host}";
|
||||
if (port > 0) host = $"{host}:{port}";
|
||||
|
||||
_baseUrl = host;
|
||||
_rootPath = path ?? "/";
|
||||
|
||||
_client = new WebDavClient(new WebDavClientParams
|
||||
{
|
||||
BaseAddress = new Uri(_baseUrl),
|
||||
Credentials = new System.Net.NetworkCredential(user, pass)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
// WebDAV 无状态,Propfind 测试根目录连通性
|
||||
var result = await _client.Propfind(_rootPath);
|
||||
return result.IsSuccessful;
|
||||
}
|
||||
|
||||
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
|
||||
{
|
||||
var targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
|
||||
var result = await _client.Propfind(targetPath);
|
||||
|
||||
var list = new List<UnifiedFileItem>();
|
||||
if (result.IsSuccessful)
|
||||
{
|
||||
foreach (var res in result.Resources)
|
||||
{
|
||||
if (res == null || res.Uri == null) continue;
|
||||
|
||||
// 排除掉文件夹自身 (WebDAV 通常会把当前请求的文件夹作为第一个结果返回)
|
||||
// 通过判断 URL 结尾是否一致来简单过滤,或者判断 IsCollection 且 Uri 相同
|
||||
// 这里简单处理:只要名字不为空
|
||||
var name = System.Net.WebUtility.UrlDecode(res.Uri.Split('/').LastOrDefault());
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
|
||||
// 如果名字和请求的目录名一样,可能是它自己,跳过 (这需要根据具体服务器响应调整)
|
||||
// 更稳妥的是比较 Uri
|
||||
|
||||
list.Add(new UnifiedFileItem
|
||||
{
|
||||
Name = name,
|
||||
FullPath = res.Uri.ToString(), // WebDAV 需要完整 URI
|
||||
IsFolder = res.IsCollection,
|
||||
Size = res.ContentLength ?? 0,
|
||||
LastModified = res.LastModifiedDate
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenReadAsync(string fullPath)
|
||||
{
|
||||
// WebDAV 获取流
|
||||
var res = await _client.GetRawFile(fullPath);
|
||||
if (!res.IsSuccessful) throw new IOException($"WebDAV Error: {res.StatusCode}");
|
||||
return res.Stream;
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync() => await Task.CompletedTask;
|
||||
|
||||
public void Dispose() => _client?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public partial class LocalMediaFolder : ObservableRecipient
|
||||
{
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Path { get; set; }
|
||||
|
||||
public LocalMediaFolder() { }
|
||||
|
||||
public LocalMediaFolder(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models.FileSystem;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public partial class MediaFolder : ObservableRecipient
|
||||
{
|
||||
[ObservableProperty] public partial string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients][NotifyPropertyChangedFor(nameof(ConnectionSummary))] public partial string Path { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedRecipients]
|
||||
[NotifyPropertyChangedFor(nameof(IsLocal))]
|
||||
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
|
||||
public partial FileSourceType SourceType { get; set; } = FileSourceType.Local;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Name { get; set; }
|
||||
|
||||
[ObservableProperty] public partial string UserName { get; set; }
|
||||
|
||||
[ObservableProperty] public partial int Port { get; set; } = -1;
|
||||
|
||||
[JsonIgnore] public string Password { get; set; }
|
||||
|
||||
[JsonIgnore] public bool IsLocal => SourceType == FileSourceType.Local;
|
||||
|
||||
[JsonIgnore]
|
||||
public string ConnectionSummary
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsLocal) return Path;
|
||||
return $"{SourceType} - {Path} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore] public string VaultKey => $"{Id}-{UserName}";
|
||||
|
||||
public MediaFolder() { }
|
||||
|
||||
public MediaFolder(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public IUnifiedFileSystem? CreateFileSystem()
|
||||
{
|
||||
if (!IsEnabled) return null;
|
||||
if (string.IsNullOrEmpty(Password) && !IsLocal)
|
||||
{
|
||||
Password = PasswordVaultHelper.Get(Constants.App.AppName, VaultKey) ?? "";
|
||||
}
|
||||
|
||||
return SourceType switch
|
||||
{
|
||||
FileSourceType.Local => new LocalFileSystem(Path),
|
||||
FileSourceType.SMB => new SMBFileSystem(Path, UserName, Password),
|
||||
FileSourceType.FTP => new FTPFileSystem(Path, UserName, Password, Port, Path),
|
||||
FileSourceType.WebDav => new WebDavFileSystem(Path, UserName, Password, Port, Path),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class PlayQueueItem
|
||||
{
|
||||
public Track Track { get; set; }
|
||||
public ExtendedTrack Track { get; set; }
|
||||
|
||||
public PlayQueueItem(Track track)
|
||||
public PlayQueueItem(ExtendedTrack track)
|
||||
{
|
||||
Track = track;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial MusicGallerySettings MusicGallerySettings { get; set; } = new MusicGallerySettings();
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AdvancedSettings AdvancedSettings { get; set; } = new AdvancedSettings();
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaFolder> LocalMediaFolders { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsWindowStatus> WindowBoundsRecords { get; set; } = [];
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
switch (provider.Provider)
|
||||
{
|
||||
case AlbumArtSearchProvider.Local:
|
||||
result = SearchFile(songInfo)?.AsBuffer();
|
||||
result = (await SearchFile(songInfo))?.AsBuffer();
|
||||
break;
|
||||
case AlbumArtSearchProvider.SMTC:
|
||||
result = bufferFromSMTC;
|
||||
@@ -77,29 +77,73 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
return null;
|
||||
}
|
||||
|
||||
private byte[]? SearchFile(SongInfo songInfo)
|
||||
private async Task<byte[]?> SearchFile(SongInfo songInfo)
|
||||
{
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
if (!folder.IsEnabled) continue;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null) continue;
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
|
||||
// 递归扫描
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue(""); // 根目录
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
Track track = new(file);
|
||||
if ((track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists) || StringHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), songInfo.DisplayArtists, songInfo.Title))
|
||||
if (item.IsFolder)
|
||||
{
|
||||
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
|
||||
if (bytes != null)
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(item.Name).ToLower();
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = await fs.OpenReadAsync(item.FullPath))
|
||||
{
|
||||
var track = new ExtendedTrack(item.FullPath, stream);
|
||||
|
||||
bool isMetadataMatch = (track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists);
|
||||
bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
|
||||
Path.GetFileNameWithoutExtension(item.Name),
|
||||
songInfo.DisplayArtists,
|
||||
songInfo.Title
|
||||
);
|
||||
|
||||
if (isMetadataMatch || isFilenameMatch)
|
||||
{
|
||||
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
|
||||
if (bytes != null && bytes.Length > 0)
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
lyricsSearchResult = await SearchAmllTtmlDbAsync(songInfo);
|
||||
break;
|
||||
case LyricsSearchProvider.LocalMusicFile:
|
||||
lyricsSearchResult = SearchEmbedded(songInfo);
|
||||
lyricsSearchResult = await SearchEmbedded(songInfo);
|
||||
break;
|
||||
case LyricsSearchProvider.LocalLrcFile:
|
||||
case LyricsSearchProvider.LocalEslrcFile:
|
||||
@@ -277,7 +277,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
private async Task<LyricsSearchResult> SearchFile(SongInfo songInfo, LyricsFormat format)
|
||||
{
|
||||
int maxScore = 0;
|
||||
string? bestFile = null;
|
||||
|
||||
MediaFolder? bestFolder = null;
|
||||
string? bestFilePath = null;
|
||||
|
||||
var lyricsSearchResult = new LyricsSearchResult();
|
||||
|
||||
@@ -288,47 +290,97 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
if (!folder.IsEnabled) continue;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null) continue;
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
|
||||
// 递归扫描
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue(""); // 从根目录开始
|
||||
|
||||
string targetExt = format.ToFileExtension();
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = file });
|
||||
if (score > maxScore)
|
||||
if (item.IsFolder)
|
||||
{
|
||||
bestFile = file;
|
||||
maxScore = score;
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Name.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FullPath });
|
||||
|
||||
if (score > maxScore)
|
||||
{
|
||||
maxScore = score;
|
||||
bestFilePath = item.FullPath;
|
||||
bestFolder = folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 日志记录...
|
||||
}
|
||||
}
|
||||
|
||||
if (bestFile != null)
|
||||
// 4. 如果找到了最佳匹配,读取内容
|
||||
if (bestFolder != null && bestFilePath != null)
|
||||
{
|
||||
lyricsSearchResult.Reference = bestFile;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
|
||||
string? raw = await File.ReadAllTextAsync(bestFile, FileHelper.GetEncoding(bestFile));
|
||||
if (raw != null)
|
||||
try
|
||||
{
|
||||
lyricsSearchResult.Raw = raw;
|
||||
// 重新连接以读取文件 (因为之前的 fs 已经在 using 结束时释放)
|
||||
using var fs = bestFolder.CreateFileSystem();
|
||||
if (fs != null && await fs.ConnectAsync())
|
||||
{
|
||||
using var stream = await fs.OpenReadAsync(bestFilePath);
|
||||
|
||||
// 使用 StreamReader 读取文本
|
||||
// 注意:这里简单使用 Default 编码,如果需要探测编码(FileHelper.GetEncoding),
|
||||
// 可能需要先读一部分字节来判断,或者使用带编码探测的库。
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string raw = await reader.ReadToEndAsync();
|
||||
|
||||
lyricsSearchResult.Reference = bestFilePath;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
lyricsSearchResult.Raw = raw;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 读取失败处理
|
||||
}
|
||||
}
|
||||
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private LyricsSearchResult SearchEmbedded(SongInfo songInfo)
|
||||
private async Task<LyricsSearchResult> SearchEmbedded(SongInfo songInfo)
|
||||
{
|
||||
int bestScore = 0;
|
||||
string? bestFile = null;
|
||||
string? bestFilePath = null;
|
||||
string? bestRaw = null;
|
||||
|
||||
// 用于最后回填 Metadata
|
||||
string? bestTitle = null;
|
||||
string[]? bestArtists = null;
|
||||
string? bestAlbum = null;
|
||||
double bestDuration = 0;
|
||||
|
||||
var lyricsSearchResult = new LyricsSearchResult
|
||||
{
|
||||
Provider = LyricsSearchProvider.LocalMusicFile,
|
||||
@@ -336,49 +388,89 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
if (!folder.IsEnabled) continue;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null) continue;
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue("");
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var track = new Track(file);
|
||||
var raw = track.GetRawLyrics();
|
||||
|
||||
if (!string.IsNullOrEmpty(raw))
|
||||
if (item.IsFolder)
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
|
||||
{
|
||||
Title = track.Title,
|
||||
Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator),
|
||||
Album = track.Album,
|
||||
Duration = track.Duration,
|
||||
Reference = file,
|
||||
});
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (score > bestScore)
|
||||
var ext = Path.GetExtension(item.Name).ToLower();
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
bestScore = score;
|
||||
bestFile = file;
|
||||
bestRaw = raw;
|
||||
using var stream = await fs.OpenReadAsync(item.FullPath);
|
||||
|
||||
var track = new ExtendedTrack(item.FullPath, stream);
|
||||
var raw = track.RawLyrics;
|
||||
|
||||
if (!string.IsNullOrEmpty(raw))
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
|
||||
{
|
||||
Title = track.Title,
|
||||
Artists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator),
|
||||
Album = track.Album,
|
||||
Duration = track.Duration,
|
||||
Reference = item.FullPath,
|
||||
});
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestFilePath = item.FullPath;
|
||||
bestRaw = raw;
|
||||
|
||||
// 缓存当前最佳的元数据,避免最后还需要重新打开文件读一次
|
||||
bestTitle = track.Title;
|
||||
bestArtists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator);
|
||||
bestAlbum = track.Album;
|
||||
bestDuration = track.Duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 单个文件解析失败忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 文件夹扫描失败忽略
|
||||
}
|
||||
}
|
||||
|
||||
if (bestFile != null)
|
||||
if (bestFilePath != null)
|
||||
{
|
||||
var track = new Track(bestFile);
|
||||
|
||||
lyricsSearchResult.Title = track.Title;
|
||||
lyricsSearchResult.Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator);
|
||||
lyricsSearchResult.Album = track.Album;
|
||||
lyricsSearchResult.Duration = track.Duration;
|
||||
// 直接使用缓存的数据,不需要 new Track(bestFile) 了
|
||||
lyricsSearchResult.Title = bestTitle;
|
||||
lyricsSearchResult.Artists = bestArtists;
|
||||
lyricsSearchResult.Album = bestAlbum;
|
||||
lyricsSearchResult.Duration = bestDuration;
|
||||
|
||||
lyricsSearchResult.Raw = bestRaw;
|
||||
lyricsSearchResult.Reference = bestFile;
|
||||
lyricsSearchResult.Reference = bestFilePath;
|
||||
lyricsSearchResult.MatchPercentage = bestScore;
|
||||
}
|
||||
|
||||
@@ -560,13 +652,14 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
}
|
||||
|
||||
ISearchResult? result;
|
||||
if (searcher == Searchers.Netease && songInfo.SongId != null)
|
||||
|
||||
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIDHelper.IsNeteaseFamily(songInfo.PlayerId))
|
||||
{
|
||||
result = new NeteaseSearchResult("", [], "", [], 0, songInfo.SongId);
|
||||
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
|
||||
}
|
||||
else if (searcher == Searchers.QQMusic && songInfo.SongId != null)
|
||||
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerID.QQMusic)
|
||||
{
|
||||
result = new QQMusicSearchResult("", [], "", [], 0, songInfo.SongId, "");
|
||||
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -120,6 +120,9 @@
|
||||
<data name="ActionCompleted" xml:space="preserve">
|
||||
<value>Operation completed</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
|
||||
<value>Local music files</value>
|
||||
</data>
|
||||
@@ -546,6 +549,21 @@
|
||||
<data name="PrivacyPolicy.Content" xml:space="preserve">
|
||||
<value>Privacy policy</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="RemoteServerConfigControlPort.Header" xml:space="preserve">
|
||||
<value>Port</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlServerAddress.Header" xml:space="preserve">
|
||||
<value>Server Address</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlUsername.Header" xml:space="preserve">
|
||||
<value>Username</value>
|
||||
</data>
|
||||
<data name="Romaji" xml:space="preserve">
|
||||
<value>Romaji</value>
|
||||
</data>
|
||||
@@ -594,9 +612,6 @@
|
||||
<data name="SettingsPageAdaptEnvColor.Header" xml:space="preserve">
|
||||
<value>Adapt to environmental color</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>Add a folder</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
@@ -1044,6 +1059,9 @@
|
||||
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
|
||||
<value>Enable monitoring for new playback sources</value>
|
||||
</data>
|
||||
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
|
||||
<value>Local Folder</value>
|
||||
</data>
|
||||
<data name="SettingsPageLog.Header" xml:space="preserve">
|
||||
<value>Log record</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,9 @@
|
||||
<data name="ActionCompleted" xml:space="preserve">
|
||||
<value>操作が完了しました</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>追加</value>
|
||||
</data>
|
||||
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
|
||||
<value>ローカル音楽ファイル</value>
|
||||
</data>
|
||||
@@ -546,6 +549,21 @@
|
||||
<data name="PrivacyPolicy.Content" 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="RemoteServerConfigControlPort.Header" xml:space="preserve">
|
||||
<value>ポート</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlServerAddress.Header" xml:space="preserve">
|
||||
<value>サーバーアドレス</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlUsername.Header" xml:space="preserve">
|
||||
<value>ユーザー名</value>
|
||||
</data>
|
||||
<data name="Romaji" xml:space="preserve">
|
||||
<value>ローマン</value>
|
||||
</data>
|
||||
@@ -594,9 +612,6 @@
|
||||
<data name="SettingsPageAdaptEnvColor.Header" xml:space="preserve">
|
||||
<value>環境の色に適応しましょう</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>フォルダーを追加します</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>追加</value>
|
||||
</data>
|
||||
@@ -1044,6 +1059,9 @@
|
||||
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
|
||||
<value>新しい再生ソースの監視を有効にします</value>
|
||||
</data>
|
||||
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
|
||||
<value>ローカル フォルダ</value>
|
||||
</data>
|
||||
<data name="SettingsPageLog.Header" xml:space="preserve">
|
||||
<value>ログレコード</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,9 @@
|
||||
<data name="ActionCompleted" xml:space="preserve">
|
||||
<value>작전 완료</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>추가하다</value>
|
||||
</data>
|
||||
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
|
||||
<value>로컬 음악 파일</value>
|
||||
</data>
|
||||
@@ -546,6 +549,21 @@
|
||||
<data name="PrivacyPolicy.Content" 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="RemoteServerConfigControlPort.Header" xml:space="preserve">
|
||||
<value>항구</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlServerAddress.Header" xml:space="preserve">
|
||||
<value>IP 주소</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlUsername.Header" xml:space="preserve">
|
||||
<value>사용자 이름</value>
|
||||
</data>
|
||||
<data name="Romaji" xml:space="preserve">
|
||||
<value>로만</value>
|
||||
</data>
|
||||
@@ -594,9 +612,6 @@
|
||||
<data name="SettingsPageAdaptEnvColor.Header" xml:space="preserve">
|
||||
<value>환경 색상에 적응하세요</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>폴더를 추가하십시오</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>추가하다</value>
|
||||
</data>
|
||||
@@ -1044,6 +1059,9 @@
|
||||
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
|
||||
<value>새로운 재생 소스에 대한 모니터링을 활성화하십시오</value>
|
||||
</data>
|
||||
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
|
||||
<value>로컬 폴더</value>
|
||||
</data>
|
||||
<data name="SettingsPageLog.Header" xml:space="preserve">
|
||||
<value>로그 레코드</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,9 @@
|
||||
<data name="ActionCompleted" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
|
||||
<value>本地音乐文件</value>
|
||||
</data>
|
||||
@@ -546,6 +549,21 @@
|
||||
<data name="PrivacyPolicy.Content" 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="RemoteServerConfigControlPort.Header" xml:space="preserve">
|
||||
<value>端口</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlServerAddress.Header" xml:space="preserve">
|
||||
<value>服务器地址</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlUsername.Header" xml:space="preserve">
|
||||
<value>用户名</value>
|
||||
</data>
|
||||
<data name="Romaji" xml:space="preserve">
|
||||
<value>罗马音</value>
|
||||
</data>
|
||||
@@ -594,9 +612,6 @@
|
||||
<data name="SettingsPageAdaptEnvColor.Header" xml:space="preserve">
|
||||
<value>适应环境色彩</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>添加文件夹</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
@@ -1044,6 +1059,9 @@
|
||||
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
|
||||
<value>启用对新播放源的监听</value>
|
||||
</data>
|
||||
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
|
||||
<value>本地文件夹</value>
|
||||
</data>
|
||||
<data name="SettingsPageLog.Header" xml:space="preserve">
|
||||
<value>日志记录</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,9 @@
|
||||
<data name="ActionCompleted" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
|
||||
<value>本地音樂文件</value>
|
||||
</data>
|
||||
@@ -546,6 +549,21 @@
|
||||
<data name="PrivacyPolicy.Content" 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="RemoteServerConfigControlPort.Header" xml:space="preserve">
|
||||
<value>連接埠</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlServerAddress.Header" xml:space="preserve">
|
||||
<value>伺服器位址</value>
|
||||
</data>
|
||||
<data name="RemoteServerConfigControlUsername.Header" xml:space="preserve">
|
||||
<value>使用者名稱</value>
|
||||
</data>
|
||||
<data name="Romaji" xml:space="preserve">
|
||||
<value>羅馬音</value>
|
||||
</data>
|
||||
@@ -594,9 +612,6 @@
|
||||
<data name="SettingsPageAdaptEnvColor.Header" xml:space="preserve">
|
||||
<value>適應環境色彩</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>新增資料夾</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
@@ -1044,6 +1059,9 @@
|
||||
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
|
||||
<value>啟用對新播放來源的監聽</value>
|
||||
</data>
|
||||
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
|
||||
<value>本地資料夾</value>
|
||||
</data>
|
||||
<data name="SettingsPageLog.Header" xml:space="preserve">
|
||||
<value>日誌記錄</value>
|
||||
</data>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Controls;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
@@ -12,6 +15,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace BetterLyrics.WinUI3.ViewModels
|
||||
{
|
||||
@@ -30,17 +34,6 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
AppSettings = _settingsService.AppSettings;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectAndAddFolderAsync(UIElement sender)
|
||||
{
|
||||
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
|
||||
|
||||
if (folder != null)
|
||||
{
|
||||
AddFolderAsync(folder.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFolderAsync(string path)
|
||||
{
|
||||
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
@@ -62,13 +55,93 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
AppSettings.LocalMediaFolders.Add(new LocalMediaFolder(path));
|
||||
AppSettings.LocalMediaFolders.Add(new MediaFolder(path));
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveFolderAsync(LocalMediaFolder folder)
|
||||
public void RemoveFolderAsync(MediaFolder folder)
|
||||
{
|
||||
AppSettings.LocalMediaFolders.Remove(folder);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
XamlRoot = WindowHook.GetWindow<SettingsWindow>()?.Content.XamlRoot,
|
||||
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
|
||||
Title = protocolType,
|
||||
PrimaryButtonText = _resourceService.GetLocalizedString("Add"),
|
||||
CloseButtonText = _resourceService.GetLocalizedString("Cancel"),
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
Content = new RemoteServerConfigControl(protocolType)
|
||||
};
|
||||
|
||||
dialog.PrimaryButtonClick += async (s, e) =>
|
||||
{
|
||||
var configControl = (RemoteServerConfigControl)dialog.Content;
|
||||
|
||||
try
|
||||
{
|
||||
e.Cancel = true;
|
||||
|
||||
dialog.IsPrimaryButtonEnabled = false;
|
||||
configControl.IsEnabled = false;
|
||||
dialog.Title = $"Connecting to {protocolType}...";
|
||||
|
||||
var tempFolder = configControl.GetConfig();
|
||||
|
||||
var provider = tempFolder.CreateFileSystem();
|
||||
|
||||
bool isConnected = provider != null && await provider.ConnectAsync();
|
||||
|
||||
if (isConnected)
|
||||
{
|
||||
await provider!.DisconnectAsync();
|
||||
|
||||
PasswordVaultHelper.Save(Constants.App.AppName, tempFolder.VaultKey, tempFolder.Password);
|
||||
AppSettings.LocalMediaFolders.Add(tempFolder);
|
||||
|
||||
e.Cancel = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowErrorTip(configControl, "Connection failed. Check IP/Port.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowErrorTip(configControl, $"Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
dialog.IsPrimaryButtonEnabled = true;
|
||||
configControl.IsEnabled = true;
|
||||
dialog.Title = protocolType;
|
||||
}
|
||||
};
|
||||
|
||||
await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void ShowErrorTip(RemoteServerConfigControl control, string message)
|
||||
{
|
||||
// 你可以在 RemoteServerConfigControl 里加一个 InfoBar 用来显示错误
|
||||
// 假设你在 UserControl 里公开了一个 ShowError 方法
|
||||
control.ShowError(message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
private readonly DispatcherQueueTimer _refreshSongsTimer;
|
||||
|
||||
// All songs
|
||||
private List<Track> _tracks = [];
|
||||
private List<ExtendedTrack> _tracks = [];
|
||||
// Songs in current playlist
|
||||
private List<Track> _playlistTracks = [];
|
||||
private List<ExtendedTrack> _playlistTracks = [];
|
||||
// Filtered songs based on search query for current playlist
|
||||
private List<Track> _filteredTracks = [];
|
||||
private List<ExtendedTrack> _filteredTracks = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppSettings AppSettings { get; set; }
|
||||
@@ -62,7 +62,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
public partial ObservableCollection<GroupInfoList> GroupedTracks { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<Track> SelectedTracks { get; set; } = [];
|
||||
public partial List<ExtendedTrack> SelectedTracks { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedTracksTotalDuration { get; set; } = 0;
|
||||
@@ -73,7 +73,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
public PlayQueueItem? PlayingQueueItem => TrackPlayingQueue.ElementAtOrDefault(AppSettings.MusicGallerySettings.PlayQueueIndex);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Track? PlayingTrack { get; set; } = null;
|
||||
public partial ExtendedTrack? PlayingTrack { get; set; } = null;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CommonSongProperty SongOrderType { get; set; } = CommonSongProperty.Title;
|
||||
@@ -90,7 +90,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
public partial bool IsDataLoading { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Track TrackRightTapped { get; set; } = new();
|
||||
public partial ExtendedTrack TrackRightTapped { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SongSearchQuery { get; set; } = string.Empty;
|
||||
@@ -103,7 +103,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
_resourceService = resourceService;
|
||||
AppSettings = _settingsService.AppSettings;
|
||||
|
||||
TrackPlayingQueue = [.. AppSettings.MusicGallerySettings.PlayQueuePaths.Select(x => new PlayQueueItem(new Track(x)))];
|
||||
TrackPlayingQueue = [.. AppSettings.MusicGallerySettings.PlayQueuePaths.Select(x => new PlayQueueItem(new ExtendedTrack(x)))];
|
||||
TrackPlayingQueue.CollectionChanged += TrackPlayingQueue_CollectionChanged;
|
||||
|
||||
SongsTabInfoList.Add(new SongsTabInfo(_resourceService.GetLocalizedString("MusicGalleryPageAllSongs"), "\uE8A9", false, false, CommonSongProperty.Title, string.Empty));
|
||||
@@ -281,28 +281,75 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
IsDataLoading = true;
|
||||
_tracks.Clear();
|
||||
|
||||
Task.Run(() =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
|
||||
{
|
||||
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
if (!folder.IsEnabled) continue;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
|
||||
using var fs = folder.CreateFileSystem();
|
||||
|
||||
if (fs == null) continue;
|
||||
|
||||
if (!await fs.ConnectAsync()) continue;
|
||||
|
||||
// 递归扫描队列
|
||||
var foldersToScan = new Queue<string>();
|
||||
foldersToScan.Enqueue(""); // 从根目录开始
|
||||
|
||||
while (foldersToScan.Count > 0)
|
||||
{
|
||||
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
|
||||
var currentPath = foldersToScan.Dequeue();
|
||||
var items = await fs.GetFilesAsync(currentPath);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
Track track = new(file);
|
||||
if (track.Duration <= 0) continue;
|
||||
_tracks.Add(track);
|
||||
if (item.IsFolder)
|
||||
{
|
||||
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(item.Name).ToLower();
|
||||
if (FileHelper.MusicExtensions.Contains(ext))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = await fs.OpenReadAsync(item.FullPath))
|
||||
{
|
||||
ExtendedTrack track = new ExtendedTrack(item.FullPath, stream);
|
||||
|
||||
if (track.Duration > 0)
|
||||
{
|
||||
// 读取专辑图写入内存
|
||||
// 因为此后该流将关闭无法再次访问
|
||||
_ = track.EmbeddedPictures;
|
||||
|
||||
_tracks.Add(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Error loading track {item.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Folder scan error ({folder.Name}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Global scan error: {ex.Message}");
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
@@ -337,7 +384,7 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
_playlistTracks = _tracks.Where(t => t.Artist.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
break;
|
||||
case CommonSongProperty.Folder:
|
||||
_playlistTracks = _tracks.Where(t => t.GetParentFolderPath().Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
_playlistTracks = _tracks.Where(t => t.ParentFolderPath.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
break;
|
||||
case CommonSongProperty.M3UFilePath:
|
||||
if (SelectedSongsTabInfo.FilterValue is string path)
|
||||
@@ -375,9 +422,9 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
t.Artist.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
t.Album.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
// 文件名(包含后缀)
|
||||
t.GetFileName().Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
t.FileName.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
// 文件所在文件夹的路径
|
||||
t.GetParentFolderPath().Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
t.ParentFolderPath.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
private void ApplySongOrderType()
|
||||
@@ -387,25 +434,25 @@ namespace BetterLyrics.WinUI3.ViewModels
|
||||
case CommonSongProperty.Title:
|
||||
GroupedTracks = _filteredTracks.GetGroupedBy(
|
||||
t => LanguageHelper.GetOrderChar(t.Title),
|
||||
o => ((Track)o).Title
|
||||
o => ((ExtendedTrack)o).Title
|
||||
);
|
||||
break;
|
||||
case CommonSongProperty.Artist:
|
||||
GroupedTracks = _filteredTracks.GetGroupedBy(
|
||||
t => LanguageHelper.GetOrderChar(t.Artist),
|
||||
o => ((Track)o).Artist
|
||||
o => ((ExtendedTrack)o).Artist
|
||||
);
|
||||
break;
|
||||
case CommonSongProperty.Album:
|
||||
GroupedTracks = _filteredTracks.GetGroupedBy(
|
||||
t => LanguageHelper.GetOrderChar(t.Album),
|
||||
o => ((Track)o).Album
|
||||
o => ((ExtendedTrack)o).Album
|
||||
);
|
||||
break;
|
||||
case CommonSongProperty.Folder:
|
||||
GroupedTracks = _filteredTracks.GetGroupedBy(
|
||||
t => LanguageHelper.GetOrderChar(t.GetParentFolderName()),
|
||||
o => ((Track)o).Album
|
||||
t => LanguageHelper.GetOrderChar(t.ParentFolderName),
|
||||
o => ((ExtendedTrack)o).Album
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
x:Uid="MusicGalleryPageFileInfoPath"
|
||||
Link="{x:Bind ViewModel.TrackRightTapped.Path, Mode=OneWay}"
|
||||
Value="{x:Bind ViewModel.TrackRightTapped.Path, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoLyrics" Value="{x:Bind ViewModel.TrackRightTapped, Converter={StaticResource TrackToLyricsConverter}, Mode=OneWay}" />
|
||||
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoLyrics" Value="{x:Bind ViewModel.TrackRightTapped.RawLyrics, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
@@ -381,7 +381,7 @@
|
||||
</MenuBarItemFlyout>
|
||||
</ListView.ContextFlyout>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="atl:Track">
|
||||
<DataTemplate x:DataType="models:ExtendedTrack">
|
||||
<Grid
|
||||
Padding="12"
|
||||
ColumnSpacing="12"
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
private async void SongPathHyperlinkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LauncherHelper.SelectAndShowFile(((Track)((HyperlinkButton)sender).DataContext).Path);
|
||||
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).Path);
|
||||
}
|
||||
|
||||
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
@@ -104,7 +104,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
private async void AddSongToQueueNextMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
|
||||
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1, SongListView.SelectedItems.Cast<Track>().Select(x => new PlayQueueItem(x)));
|
||||
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1, SongListView.SelectedItems.Cast<ExtendedTrack>().Select(x => new PlayQueueItem(x)));
|
||||
if (startPlaying)
|
||||
{
|
||||
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1;
|
||||
@@ -115,7 +115,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
private async void AddSongToQueueEndMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
|
||||
ViewModel.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<Track>().Select(x => new PlayQueueItem(x)));
|
||||
ViewModel.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<ExtendedTrack>().Select(x => new PlayQueueItem(x)));
|
||||
if (startPlaying)
|
||||
{
|
||||
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1;
|
||||
@@ -125,7 +125,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
private void SongListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
ViewModel.SelectedTracks = SongListView.SelectedItems.Cast<Track>().ToList();
|
||||
ViewModel.SelectedTracks = SongListView.SelectedItems.Cast<ExtendedTrack>().ToList();
|
||||
ViewModel.SelectedTracksTotalDuration = ViewModel.SelectedTracks.Select(x => x.Duration).Sum();
|
||||
if (SelectAllCheckBox != null)
|
||||
{
|
||||
@@ -142,22 +142,22 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
private void ArtistHyperlibkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var artist = ((Track)((FrameworkElement)sender).DataContext).Artist;
|
||||
var artist = ((ExtendedTrack)((FrameworkElement)sender).DataContext).Artist;
|
||||
var playlist = new SongsTabInfo(artist, "\uEFA9", true, false, CommonSongProperty.Artist, artist);
|
||||
ViewModel.UpdateSelectedPlaylist(playlist);
|
||||
}
|
||||
|
||||
private void AlbumHyperlibkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var album = ((Track)((FrameworkElement)sender).DataContext).Album;
|
||||
var album = ((ExtendedTrack)((FrameworkElement)sender).DataContext).Album;
|
||||
var playlist = new SongsTabInfo(album, "\uE93C", true, false, CommonSongProperty.Album, album);
|
||||
ViewModel.UpdateSelectedPlaylist(playlist);
|
||||
}
|
||||
|
||||
private void PathHyperlibkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var track = ((Track)((FrameworkElement)sender).DataContext);
|
||||
var playlist = new SongsTabInfo(track.GetParentFolderName(), "\uE8B7", true, false, CommonSongProperty.Folder, track.GetParentFolderPath());
|
||||
var track = ((ExtendedTrack)((FrameworkElement)sender).DataContext);
|
||||
var playlist = new SongsTabInfo(track.ParentFolderName, "\uE8B7", true, false, CommonSongProperty.Folder, track.ParentFolderPath);
|
||||
ViewModel.UpdateSelectedPlaylist(playlist);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
private void SongListViewItemMoreButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.TrackRightTapped = (Track)((FrameworkElement)sender).DataContext;
|
||||
ViewModel.TrackRightTapped = (ExtendedTrack)((FrameworkElement)sender).DataContext;
|
||||
SongFileInfoFlyout.ShowAt(sender as FrameworkElement);
|
||||
}
|
||||
|
||||
@@ -260,8 +260,8 @@ namespace BetterLyrics.WinUI3.Views
|
||||
|
||||
private async void SongListViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
{
|
||||
var displayedTracks = SongListView.Items.Cast<Track>();
|
||||
var track = (Track)((FrameworkElement)sender).DataContext;
|
||||
var displayedTracks = SongListView.Items.Cast<ExtendedTrack>();
|
||||
var track = (ExtendedTrack)((FrameworkElement)sender).DataContext;
|
||||
|
||||
// Play all the songs
|
||||
ViewModel.TrackPlayingQueue.Clear();
|
||||
|
||||
Reference in New Issue
Block a user