.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dbd78f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# 哔哩下载姬
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+哔哩下载姬(DownKyi)是一个简单易用的哔哩哔哩视频下载工具,具有简洁的界面,流畅的操作逻辑。哔哩下载姬可以下载几乎所有的B站视频,并输出mp4格式的文件;采用Aria下载器多线程下载,采用FFmpeg对视频进行混流、提取音视频等操作。
+
+[更多详情](src/README.md)
+
+## 下载
+
+
+
+
+
+
+
+
+
+
+
+
+
+[更新日志](CHANGELOG.md)
+
+## 问题
+
+- Aria下载失败:检查aria2c.exe是否可以正常工作和是否允许通过防火墙;或者尝试切换端口号。
+- 内建下载器失败:请提issue,也欢迎pr。
+- 下载时卡在“混流中”:检查ffmpeg.exe是否可以正常工作。
+- 去水印:宽/高为水印的尺寸,X/Y为水印在图像中的位置(以左上角为原点),这四个数据可通过Photoshop获得。
+
+## 赞助
+
+如果这个项目对您有很大帮助,并且您希望支持该项目的开发和维护,请随时扫描一下二维码进行捐赠。非常感谢您的捐款,谢谢!
+
+
+
+## 开发
+
+### x86 & x64
+
+发布的压缩包中aria2c.exe和ffmpeg.exe均为32位,如果需要请用下面链接中的文件替换。
+
+- [aria2-1.36.0-win-32bit](third_party/aria2-1.36.0-win-32bit-build1.zip)
+- [aria2-1.36.0-win-64bit](third_party/aria2-1.36.0-win-64bit-build1.zip)
+- [FFmpeg](https://github.com/leiurayer/FFmpeg-Builds/releases/tag/latest)
+
+### 相关项目
+
+- [哔哩哔哩-API收集整理](https://github.com/SocialSisterYi/bilibili-API-collect):B站API归档
+- [Prism](https://github.com/PrismLibrary/Prism):MVVM框架
+- [WebPSharp](https://github.com/leiurayer/WebPSharp):WebP格式图片支持,[NuGet程序包](third_party/WebPSharp.0.5.1.nupkg)
+
+## 免责申明
+
+1. 本软件只提供视频解析,不提供任何资源上传、存储到服务器的功能。
+2. 本软件仅解析来自B站的内容,不会对解析到的音视频进行二次编码,部分视频会进行有限的格式转换、拼接等操作。
+3. 本软件解析得到的所有内容均来自B站UP主上传、分享,其版权均归原作者所有。内容提供者、上传者(UP主)应对其提供、上传的内容承担全部责任。
+4. **本软件提供的所有内容,仅可用作学习交流使用,未经原作者授权,禁止用于其他用途。请在下载24小时内删除。为尊重作者版权,请前往资源的原始发布网站观看,支持原创,谢谢。**
+5. 因使用本软件产生的版权问题,软件作者概不负责。
diff --git a/images/Alipay.png b/images/Alipay.png
new file mode 100644
index 0000000..f33b7db
Binary files /dev/null and b/images/Alipay.png differ
diff --git a/images/WeChat.png b/images/WeChat.png
new file mode 100644
index 0000000..8ef8a02
Binary files /dev/null and b/images/WeChat.png differ
diff --git a/images/app/index.png b/images/app/index.png
new file mode 100644
index 0000000..a950b9a
Binary files /dev/null and b/images/app/index.png differ
diff --git a/src/.gitattributes b/src/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/src/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..9491a2f
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1,363 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/Api/Login/TestLoginInfo.cs b/src/BiliSharp.UnitTest/Api/Login/TestLoginInfo.cs
new file mode 100644
index 0000000..84c50d0
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Api/Login/TestLoginInfo.cs
@@ -0,0 +1,35 @@
+using BiliSharp.Api.Login;
+
+namespace BiliSharp.UnitTest.Api.Login;
+
+public class TestLoginInfo
+{
+ [Fact]
+ public void TestGetNavigationInfo_Default()
+ {
+ Cookies.GetMyCookies();
+
+ long mid = 42018135;
+ var info = LoginInfo.GetNavigationInfo();
+ Assert.Equal(mid, info.Data.Mid);
+ }
+
+ [Fact]
+ public void TestGetLoginInfoStat_Default()
+ {
+ Cookies.GetMyCookies();
+
+ var stat = LoginInfo.GetLoginInfoStat();
+ Assert.NotNull(stat);
+ }
+
+ [Fact]
+ public void TestGetMyCoin_Default()
+ {
+ Cookies.GetMyCookies();
+
+ var coin = LoginInfo.GetMyCoin();
+ Assert.NotNull(coin);
+ }
+
+}
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/Api/Login/TestLoginNotice.cs b/src/BiliSharp.UnitTest/Api/Login/TestLoginNotice.cs
new file mode 100644
index 0000000..1130ec4
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Api/Login/TestLoginNotice.cs
@@ -0,0 +1,16 @@
+using BiliSharp.Api.Login;
+
+namespace BiliSharp.UnitTest.Api.Login;
+
+public class TestLoginNotice
+{
+ [Fact]
+ public void TestGetLoginNotice_Default()
+ {
+ Cookies.GetMyCookies();
+
+ long mid = 42018135;
+ var notice = LoginNotice.GetLoginNotice(mid);
+ Assert.Equal(mid, notice.Data.Mid);
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/Api/Login/TestLoginQR.cs b/src/BiliSharp.UnitTest/Api/Login/TestLoginQR.cs
new file mode 100644
index 0000000..4c21328
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Api/Login/TestLoginQR.cs
@@ -0,0 +1,23 @@
+using BiliSharp.Api.Login;
+
+namespace BiliSharp.UnitTest.Api.Login;
+
+public class TestLoginQR
+{
+ [Fact]
+ public void TestGenerateQRCode_Default()
+ {
+ var qrcode = LoginQR.GenerateQRCode();
+ Assert.NotNull(qrcode.Data);
+ }
+
+ [Fact]
+ public void TestPollQRCode_Default()
+ {
+ var qrcode = LoginQR.GenerateQRCode();
+ Assert.NotNull(qrcode.Data);
+
+ var poll = LoginQR.PollQRCode(qrcode.Data.QrcodeKey);
+ Assert.NotNull(poll.Data);
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/Api/Sign/TestWbiSign.cs b/src/BiliSharp.UnitTest/Api/Sign/TestWbiSign.cs
new file mode 100644
index 0000000..93231b8
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Api/Sign/TestWbiSign.cs
@@ -0,0 +1,87 @@
+using BiliSharp.Api.Sign;
+
+namespace BiliSharp.UnitTest.Api.Sign
+{
+ public class TestWbiSign
+ {
+ [Fact]
+ public void TestParametersToQuery_Default()
+ {
+ var parameters = new Dictionary
+ {
+ { "fourk", 1 },
+ { "fnver", 0 },
+ { "fnval", 4048 },
+ { "cid", 405595939 },
+ { "qn", 120 },
+ { "bvid", "BV1Sg411F7cb" },
+ { "aid", 505421421 }
+ };
+ string expected = "fourk=1&fnver=0&fnval=4048&cid=405595939&qn=120&bvid=BV1Sg411F7cb&aid=505421421";
+
+ string query = WbiSign.ParametersToQuery(parameters);
+ Assert.Equal(expected, query);
+ }
+
+ [Fact]
+ public void TestParametersToQuery_Empty()
+ {
+ var parameters = new Dictionary();
+ string query = WbiSign.ParametersToQuery(parameters);
+ Assert.Equal("", query);
+ }
+
+ [Fact]
+ public void TestKeys_Default()
+ {
+ var key1 = WbiSign.GetKey();
+ Assert.NotNull(key1);
+
+ string imgKey = "34478ba821254d9d93542680e3b86100";
+ string subKey = "7e16a90d190a4355a78fd00b32a38de6";
+ var keys = new Tuple(imgKey, subKey);
+ WbiSign.SetKey(keys);
+
+ var key2 = WbiSign.GetKey();
+ Assert.Equal(imgKey, key2.Item1);
+ Assert.Equal(subKey, key2.Item2);
+ }
+
+ [Fact]
+ public void TestEncodeWbi_Default()
+ {
+ var parameters = new Dictionary
+ {
+ { "fourk", 1 },
+ { "fnver", 0 },
+ { "fnval", 4048 },
+ { "cid", 405595939 },
+ { "qn", 120 },
+ { "bvid", "BV1Sg411F7cb" },
+ { "aid", 505421421 }
+ };
+
+ var wbi = WbiSign.EncodeWbi(parameters);
+ Assert.NotNull(wbi["w_rid"]);
+ }
+
+ [Fact]
+ public void TestEncodeWbi_Default2()
+ {
+ var parameters = new Dictionary
+ {
+ { "fourk", 1 },
+ { "fnver", 0 },
+ { "fnval", 4048 },
+ { "cid", 405595939 },
+ { "qn", 120 },
+ { "bvid", "BV1Sg411F7cb" },
+ { "aid", 505421421 }
+ };
+
+ var wbi = WbiSign.EncodeWbi(parameters, "653657f524a547ac981ded72ea172057", "6e4909c702f846728e64f6007736a338");
+ Assert.NotNull(wbi["w_rid"]);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/Api/User/TestNickname.cs b/src/BiliSharp.UnitTest/Api/User/TestNickname.cs
new file mode 100644
index 0000000..9712b06
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Api/User/TestNickname.cs
@@ -0,0 +1,25 @@
+using BiliSharp.Api.User;
+
+namespace BiliSharp.UnitTest.Api.User
+{
+ public class TestNickname
+ {
+ [Fact]
+ public void TestCheckNickname_Default()
+ {
+ Equal("downkyi", 0);
+ Equal("maozedong", 40002);
+ Equal("//", 40004);
+ Equal("test0000000000000", 40005);
+ Equal("0", 40006);
+ Equal("test", 40014);
+ }
+
+ private static void Equal(string nickname, int code)
+ {
+ var response = Nickname.CheckNickname(nickname);
+ Assert.Equal(code, response.Code);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/Api/User/TestUserInfo.cs b/src/BiliSharp.UnitTest/Api/User/TestUserInfo.cs
new file mode 100644
index 0000000..5b65f53
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Api/User/TestUserInfo.cs
@@ -0,0 +1,70 @@
+using BiliSharp.Api.Login;
+using BiliSharp.Api.Sign;
+using BiliSharp.Api.User;
+
+namespace BiliSharp.UnitTest.Api.User
+{
+ public class TestUserInfo
+ {
+ [Fact]
+ public void TestGetUserInfo_Default()
+ {
+ Cookies.GetMyCookies();
+
+ // 设置wbi keys
+ var info = LoginInfo.GetNavigationInfo();
+ var imgKey = info.Data.WbiImg.ImgUrl.Split('/').ToList().Last().Split('.')[0];
+ var subKey = info.Data.WbiImg.SubUrl.Split('/').ToList().Last().Split('.')[0];
+ var keys = new Tuple(imgKey, subKey);
+ WbiSign.SetKey(keys);
+
+ long mid = 42018135;
+ var userInfo = UserInfo.GetUserInfo(mid);
+
+ Assert.Equal(mid, userInfo.Data.Mid);
+ }
+
+ [Fact]
+ public void TestGetUserCard_Default()
+ {
+ Cookies.GetMyCookies();
+
+ long mid = 42018135;
+ var userCard = UserInfo.GetUserCard(mid);
+
+ Assert.Equal(mid.ToString(), userCard.Data.Card.Mid);
+ }
+
+ [Fact]
+ public void TestGetMyInfo_Default()
+ {
+ Cookies.GetMyCookies();
+
+ long mid = 42018135;
+ var myInfo = UserInfo.GetMyInfo();
+
+ Assert.Equal(mid, myInfo.Data.Mid);
+ }
+
+ [Fact]
+ public void TestGetUserCards_Default()
+ {
+ Cookies.GetMyCookies();
+
+ // https://api.vc.bilibili.com/account/v1/user/cards?uids=314521322,206840230,49246269
+ List ids = new()
+ {
+ 314521322,
+ 206840230,
+ 49246269
+ };
+ var users = UserInfo.GetUserCards(ids);
+
+ foreach (var user in users.Data)
+ {
+ Assert.Contains(user.Mid, ids);
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp.UnitTest/BiliSharp.UnitTest.csproj b/src/BiliSharp.UnitTest/BiliSharp.UnitTest.csproj
new file mode 100644
index 0000000..51bfa56
--- /dev/null
+++ b/src/BiliSharp.UnitTest/BiliSharp.UnitTest.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/BiliSharp.UnitTest/Cookies.cs b/src/BiliSharp.UnitTest/Cookies.cs
new file mode 100644
index 0000000..f437dda
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Cookies.cs
@@ -0,0 +1,43 @@
+using System.Net;
+
+namespace BiliSharp.UnitTest;
+
+public static class Cookies
+{
+ ///
+ /// 解析从浏览器获取的cookies,用于设置cookie
+ ///
+ ///
+ ///
+ public static CookieContainer ParseCookieByString(string str)
+ {
+ var cookieContainer = new CookieContainer();
+
+ var cookies = str.Replace(" ", "").Split(";");
+ foreach (var cookie in cookies)
+ {
+ DateTime dateTime = DateTime.Now;
+ dateTime = dateTime.AddMonths(12);
+
+ var temp = cookie.Split("=");
+ var name = temp[0];
+ var value = temp[1];
+
+ // 添加cookie
+ cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime });
+ }
+ return cookieContainer;
+ }
+
+ public static void GetMyCookies()
+ {
+ string cookiesStr = "DedeUserID=42018135; " +
+ "DedeUserID__ckMd5=44e22fa30fe34ac4; " +
+ "SESSDATA=32c16297%2C1700815953%2Cb11cd%2A51; " +
+ "bili_jct=98dbd091dc07d8f9b69ba3845974e7c8; " +
+ "sid=6vomjg3u";
+ var cookies = ParseCookieByString(cookiesStr);
+ BiliManager.Instance().SetCookies(cookies);
+ }
+
+}
diff --git a/src/BiliSharp.UnitTest/Usings.cs b/src/BiliSharp.UnitTest/Usings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/src/BiliSharp.UnitTest/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Login/LoginInfo.cs b/src/BiliSharp/Api/Login/LoginInfo.cs
new file mode 100644
index 0000000..6f204c2
--- /dev/null
+++ b/src/BiliSharp/Api/Login/LoginInfo.cs
@@ -0,0 +1,40 @@
+using BiliSharp.Api.Models.Login;
+
+namespace BiliSharp.Api.Login
+{
+ ///
+ /// 登录基本信息
+ ///
+ public static class LoginInfo
+ {
+ ///
+ /// 导航栏用户信息
+ ///
+ ///
+ public static NavigationLoginInfo GetNavigationInfo()
+ {
+ string url = "https://api.bilibili.com/x/web-interface/nav";
+ return Utils.GetData(url);
+ }
+
+ ///
+ /// 登录用户状态数(双端)
+ ///
+ ///
+ public static LoginInfoStat GetLoginInfoStat()
+ {
+ string url = "https://api.bilibili.com/x/web-interface/nav/stat";
+ return Utils.GetData(url);
+ }
+
+ ///
+ /// 获取硬币数
+ ///
+ ///
+ public static MyCoin GetMyCoin()
+ {
+ string url = "https://account.bilibili.com/site/getCoin";
+ return Utils.GetData(url);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Login/LoginNotice.cs b/src/BiliSharp/Api/Login/LoginNotice.cs
new file mode 100644
index 0000000..c896248
--- /dev/null
+++ b/src/BiliSharp/Api/Login/LoginNotice.cs
@@ -0,0 +1,18 @@
+namespace BiliSharp.Api.Login
+{
+ ///
+ /// 登录记录
+ ///
+ public static class LoginNotice
+ {
+ ///
+ /// 查询登录记录
+ ///
+ ///
+ public static Models.Login.LoginNotice GetLoginNotice(long mid)
+ {
+ string url = $"https://api.bilibili.com/x/safecenter/login_notice?mid={mid}";
+ return Utils.GetData(url);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Login/LoginQR.cs b/src/BiliSharp/Api/Login/LoginQR.cs
new file mode 100644
index 0000000..e5fc29b
--- /dev/null
+++ b/src/BiliSharp/Api/Login/LoginQR.cs
@@ -0,0 +1,30 @@
+using BiliSharp.Api.Models.Login;
+
+namespace BiliSharp.Api.Login
+{
+ ///
+ /// 二维码登录
+ ///
+ public static class LoginQR
+ {
+ ///
+ /// 申请二维码(web端)
+ ///
+ ///
+ public static LoginQRCode GenerateQRCode()
+ {
+ string url = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate";
+ return Utils.GetData(url);
+ }
+
+ ///
+ /// 扫码登录(web端)
+ ///
+ ///
+ public static LoginQRCodePoll PollQRCode(string qrcodeKey)
+ {
+ string url = $"https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qrcodeKey}";
+ return Utils.GetData(url);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/Login/LoginInfoStat.cs b/src/BiliSharp/Api/Models/Login/LoginInfoStat.cs
new file mode 100644
index 0000000..eaa79cc
--- /dev/null
+++ b/src/BiliSharp/Api/Models/Login/LoginInfoStat.cs
@@ -0,0 +1,58 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.Login
+{
+ ///
+ /// https://api.bilibili.com/x/web-interface/nav/stat
+ ///
+ public class LoginInfoStat
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public LoginInfoStatData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class LoginInfoStatData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("following")]
+ public int Following { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("follower")]
+ public int Follower { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("dynamic_count")]
+ public int DynamicCount { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/Login/LoginNotice.cs b/src/BiliSharp/Api/Models/Login/LoginNotice.cs
new file mode 100644
index 0000000..453c1bf
--- /dev/null
+++ b/src/BiliSharp/Api/Models/Login/LoginNotice.cs
@@ -0,0 +1,76 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.Login
+{
+ ///
+ /// https://api.bilibili.com/x/safecenter/login_notice?mid=
+ ///
+ public class LoginNotice
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public LoginNoticeData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class LoginNoticeData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("device_name")]
+ public string DeviceName { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("login_type")]
+ public string LoginType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("login_time")]
+ public string LoginTime { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("location")]
+ public string Location { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ip")]
+ public string Ip { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/Login/LoginQRCode.cs b/src/BiliSharp/Api/Models/Login/LoginQRCode.cs
new file mode 100644
index 0000000..40921fb
--- /dev/null
+++ b/src/BiliSharp/Api/Models/Login/LoginQRCode.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.Login
+{
+ ///
+ /// https://passport.bilibili.com/x/passport-login/web/qrcode/generate
+ ///
+ public class LoginQRCode
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public LoginQRCodeData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class LoginQRCodeData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("url")]
+ public string Url { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("qrcode_key")]
+ public string QrcodeKey { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/Login/LoginQRCodePoll.cs b/src/BiliSharp/Api/Models/Login/LoginQRCodePoll.cs
new file mode 100644
index 0000000..e183d69
--- /dev/null
+++ b/src/BiliSharp/Api/Models/Login/LoginQRCodePoll.cs
@@ -0,0 +1,70 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.Login
+{
+ ///
+ /// https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=
+ ///
+ public class LoginQRCodePoll
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public LoginQRCodePollData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class LoginQRCodePollData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("url")]
+ public string Url { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("refresh_token")]
+ public string RefreshToken { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("timestamp")]
+ public long Timestamp { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public long Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/Login/MyCoin.cs b/src/BiliSharp/Api/Models/Login/MyCoin.cs
new file mode 100644
index 0000000..b7a9d7a
--- /dev/null
+++ b/src/BiliSharp/Api/Models/Login/MyCoin.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.Login
+{
+ ///
+ /// https://account.bilibili.com/site/getCoin
+ ///
+ public class MyCoin
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("status")]
+ public bool Status { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public MyCoinData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyCoinData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("money")]
+ public int Money { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/Login/NavigationLoginInfo.cs b/src/BiliSharp/Api/Models/Login/NavigationLoginInfo.cs
new file mode 100644
index 0000000..6475320
--- /dev/null
+++ b/src/BiliSharp/Api/Models/Login/NavigationLoginInfo.cs
@@ -0,0 +1,556 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.Login
+{
+ ///
+ /// https://api.bilibili.com/x/web-interface/nav
+ ///
+ public class NavigationLoginInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public NavigationLoginInfoData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("isLogin")]
+ public bool Islogin { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("email_verified")]
+ public int EmailVerified { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face")]
+ public string Face { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft")]
+ public int FaceNft { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft_type")]
+ public int FaceNftType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level_info")]
+ public NavigationLoginInfoDataLevelInfo LevelInfo { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("mobile_verified")]
+ public int MobileVerified { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("money")]
+ public int Money { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("moral")]
+ public int Moral { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("official")]
+ public NavigationLoginInfoDataOfficial Official { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("officialVerify")]
+ public NavigationLoginInfoDataOfficialverify Officialverify { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("pendant")]
+ public NavigationLoginInfoDataPendant Pendant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("scores")]
+ public int Scores { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("uname")]
+ public string Uname { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vipDueDate")]
+ public long Vipduedate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vipStatus")]
+ public int Vipstatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vipType")]
+ public int Viptype { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_pay_type")]
+ public int VipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_theme_type")]
+ public int VipThemeType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_label")]
+ public NavigationLoginInfoDataVipLabel VipLabel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_avatar_subscript")]
+ public int VipAvatarSubscript { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_nickname_color")]
+ public string VipNicknameColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip")]
+ public NavigationLoginInfoDataVip Vip { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("wallet")]
+ public NavigationLoginInfoDataWallet Wallet { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("has_shop")]
+ public bool HasShop { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("shop_url")]
+ public string ShopUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("allowance_count")]
+ public int AllowanceCount { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("answer_status")]
+ public int AnswerStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_senior_member")]
+ public int IsSeniorMember { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("wbi_img")]
+ public NavigationLoginInfoDataWbiImg WbiImg { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_jury")]
+ public bool IsJury { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataLevelInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_level")]
+ public int CurrentLevel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_min")]
+ public int CurrentMin { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_exp")]
+ public int CurrentExp { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("next_exp")]
+ public object NextExp { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataOfficial
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataOfficialverify
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataPendant
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("pid")]
+ public int Pid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("expire")]
+ public int Expire { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance")]
+ public string ImageEnhance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance_frame")]
+ public string ImageEnhanceFrame { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataVipLabel
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label_theme")]
+ public string LabelTheme { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_color")]
+ public string TextColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_style")]
+ public int BgStyle { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_color")]
+ public string BgColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("border_color")]
+ public string BorderColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("use_img_label")]
+ public bool UseImgLabel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans")]
+ public string ImgLabelUriHans { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant")]
+ public string ImgLabelUriHant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans_static")]
+ public string ImgLabelUriHansStatic { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant_static")]
+ public string ImgLabelUriHantStatic { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataVip
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("due_date")]
+ public long DueDate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_pay_type")]
+ public int VipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("theme_type")]
+ public int ThemeType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label")]
+ public NavigationLoginInfoDataVipLabel Label { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript")]
+ public int AvatarSubscript { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nickname_color")]
+ public string NicknameColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript_url")]
+ public string AvatarSubscriptUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_status")]
+ public int TvVipStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_pay_type")]
+ public int TvVipPayType { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataWallet
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bcoin_balance")]
+ public int BcoinBalance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("coupon_balance")]
+ public int CouponBalance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("coupon_due_time")]
+ public int CouponDueTime { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class NavigationLoginInfoDataWbiImg
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_url")]
+ public string ImgUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sub_url")]
+ public string SubUrl { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/User/MyInfo.cs b/src/BiliSharp/Api/Models/User/MyInfo.cs
new file mode 100644
index 0000000..cfb71cd
--- /dev/null
+++ b/src/BiliSharp/Api/Models/User/MyInfo.cs
@@ -0,0 +1,652 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.User
+{
+ ///
+ /// https://api.bilibili.com/x/space/myinfo
+ ///
+ public class MyInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public MyInfoData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sex")]
+ public string Sex { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face")]
+ public string Face { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sign")]
+ public string Sign { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("rank")]
+ public int Rank { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public int Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("jointime")]
+ public int Jointime { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("moral")]
+ public int Moral { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("silence")]
+ public int Silence { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("email_status")]
+ public int EmailStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tel_status")]
+ public int TelStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("identification")]
+ public int Identification { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip")]
+ public MyInfoDataVip Vip { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("pendant")]
+ public MyInfoDataPendant Pendant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nameplate")]
+ public MyInfoDataNameplate Nameplate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("official")]
+ public MyInfoDataOfficial Official { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("birthday")]
+ public long Birthday { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_tourist")]
+ public int IsTourist { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_fake_account")]
+ public int IsFakeAccount { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("pin_prompting")]
+ public int PinPrompting { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_deleted")]
+ public int IsDeleted { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("in_reg_audit")]
+ public int InRegAudit { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_rip_user")]
+ public bool IsRipUser { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("profession")]
+ public MyInfoDataProfession Profession { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft")]
+ public int FaceNft { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft_new")]
+ public int FaceNftNew { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_senior_member")]
+ public int IsSeniorMember { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("honours")]
+ public MyInfoDataHonours Honours { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("digital_id")]
+ public string DigitalId { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("digital_type")]
+ public int DigitalType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level_exp")]
+ public MyInfoDataLevelExp LevelExp { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("coins")]
+ public int Coins { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("following")]
+ public int Following { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("follower")]
+ public int Follower { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataVip
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("due_date")]
+ public long DueDate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_pay_type")]
+ public int VipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("theme_type")]
+ public int ThemeType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label")]
+ public MyInfoDataVipLabel Label { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript")]
+ public int AvatarSubscript { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nickname_color")]
+ public string NicknameColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript_url")]
+ public string AvatarSubscriptUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_status")]
+ public int TvVipStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_pay_type")]
+ public int TvVipPayType { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataVipLabel
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label_theme")]
+ public string LabelTheme { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_color")]
+ public string TextColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_style")]
+ public int BgStyle { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_color")]
+ public string BgColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("border_color")]
+ public string BorderColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("use_img_label")]
+ public bool UseImgLabel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans")]
+ public string ImgLabelUriHans { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant")]
+ public string ImgLabelUriHant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans_static")]
+ public string ImgLabelUriHansStatic { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant_static")]
+ public string ImgLabelUriHantStatic { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataPendant
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("pid")]
+ public int Pid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("expire")]
+ public int Expire { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance")]
+ public string ImageEnhance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance_frame")]
+ public string ImageEnhanceFrame { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataNameplate
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("nid")]
+ public int Nid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_small")]
+ public string ImageSmall { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public string Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("condition")]
+ public string Condition { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataOfficial
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataProfession
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("id")]
+ public int Id { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("show_name")]
+ public string ShowName { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_show")]
+ public int IsShow { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("category_one")]
+ public string CategoryOne { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("realname")]
+ public string Realname { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("department")]
+ public string Department { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataHonours
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("colour")]
+ public MyInfoDataHonoursColour Colour { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tags")]
+ public object Tags { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataHonoursColour
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("dark")]
+ public string Dark { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("normal")]
+ public string Normal { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class MyInfoDataLevelExp
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_level")]
+ public int CurrentLevel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_min")]
+ public int CurrentMin { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_exp")]
+ public int CurrentExp { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("next_exp")]
+ public int NextExp { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level_up")]
+ public long LevelUp { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/User/Nickname.cs b/src/BiliSharp/Api/Models/User/Nickname.cs
new file mode 100644
index 0000000..8c08569
--- /dev/null
+++ b/src/BiliSharp/Api/Models/User/Nickname.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.User
+{
+ ///
+ /// https://passport.bilibili.com/web/generic/check/nickname?nickName=hahaha
+ ///
+ public class Nickname
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/User/UserCard.cs b/src/BiliSharp/Api/Models/User/UserCard.cs
new file mode 100644
index 0000000..090b0c3
--- /dev/null
+++ b/src/BiliSharp/Api/Models/User/UserCard.cs
@@ -0,0 +1,599 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.User
+{
+ ///
+ /// https://api.bilibili.com/x/web-interface/card?mid=314521322&photo=true
+ ///
+ public class UserCard
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public UserCardData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("card")]
+ public UserCardDataCard Card { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("space")]
+ public UserCardDataSpace Space { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("following")]
+ public bool Following { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("archive_count")]
+ public int ArchiveCount { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("article_count")]
+ public int ArticleCount { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("follower")]
+ public long Follower { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("like_num")]
+ public long LikeNum { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCard
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public string Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("approve")]
+ public bool Approve { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sex")]
+ public string Sex { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("rank")]
+ public string Rank { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face")]
+ public string Face { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft")]
+ public int FaceNft { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft_type")]
+ public int FaceNftType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("DisplayRank")]
+ public string Displayrank { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("regtime")]
+ public int Regtime { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("spacesta")]
+ public int Spacesta { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("birthday")]
+ public string Birthday { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("place")]
+ public string Place { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("description")]
+ public string Description { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("article")]
+ public int Article { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("attentions")]
+ public List Attentions { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("fans")]
+ public long Fans { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("friend")]
+ public int Friend { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("attention")]
+ public int Attention { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sign")]
+ public string Sign { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level_info")]
+ public UserCardDataCardLevelInfo LevelInfo { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("pendant")]
+ public UserCardDataCardPendant Pendant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nameplate")]
+ public UserCardDataCardNameplate Nameplate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("Official")]
+ public UserCardDataCardOfficial Official { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("official_verify")]
+ public UserCardDataCardOfficialVerify OfficialVerify { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip")]
+ public UserCardDataCardVip Vip { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_senior_member")]
+ public int IsSeniorMember { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardLevelInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_level")]
+ public int CurrentLevel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_min")]
+ public int CurrentMin { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("current_exp")]
+ public int CurrentExp { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("next_exp")]
+ public int NextExp { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardPendant
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("pid")]
+ public int Pid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("expire")]
+ public int Expire { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance")]
+ public string ImageEnhance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance_frame")]
+ public string ImageEnhanceFrame { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardNameplate
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("nid")]
+ public int Nid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_small")]
+ public string ImageSmall { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public string Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("condition")]
+ public string Condition { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardOfficial
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardOfficialVerify
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardVip
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("due_date")]
+ public long DueDate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_pay_type")]
+ public int VipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("theme_type")]
+ public int ThemeType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label")]
+ public UserCardDataCardVipLabel Label { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript")]
+ public int AvatarSubscript { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nickname_color")]
+ public string NicknameColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript_url")]
+ public string AvatarSubscriptUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_status")]
+ public int TvVipStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_pay_type")]
+ public int TvVipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vipType")]
+ public int Viptype { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vipStatus")]
+ public int Vipstatus { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataCardVipLabel
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label_theme")]
+ public string LabelTheme { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_color")]
+ public string TextColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_style")]
+ public int BgStyle { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_color")]
+ public string BgColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("border_color")]
+ public string BorderColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("use_img_label")]
+ public bool UseImgLabel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans")]
+ public string ImgLabelUriHans { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant")]
+ public string ImgLabelUriHant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans_static")]
+ public string ImgLabelUriHansStatic { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant_static")]
+ public string ImgLabelUriHantStatic { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardDataSpace
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("s_img")]
+ public string SImg { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("l_img")]
+ public string LImg { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/User/UserCards.cs b/src/BiliSharp/Api/Models/User/UserCards.cs
new file mode 100644
index 0000000..2ce52fe
--- /dev/null
+++ b/src/BiliSharp/Api/Models/User/UserCards.cs
@@ -0,0 +1,443 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.User
+{
+ ///
+ /// https://api.vc.bilibili.com/account/v1/user/cards?uids=314521322,206840230,49246269
+ ///
+ public class UserCards
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("msg")]
+ public string Msg { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public List Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardsData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sex")]
+ public string Sex { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face")]
+ public string Face { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sign")]
+ public string Sign { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("rank")]
+ public int Rank { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public int Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("silence")]
+ public int Silence { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip")]
+ public UserCardsDataVip Vip { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("pendant")]
+ public UserCardsDataPendant Pendant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nameplate")]
+ public UserCardsDataNameplate Nameplate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("official")]
+ public UserCardsDataOfficial Official { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("birthday")]
+ public long Birthday { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_fake_account")]
+ public int IsFakeAccount { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_deleted")]
+ public int IsDeleted { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("in_reg_audit")]
+ public int InRegAudit { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft")]
+ public int FaceNft { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft_new")]
+ public int FaceNftNew { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_senior_member")]
+ public int IsSeniorMember { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("digital_id")]
+ public string DigitalId { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("digital_type")]
+ public int DigitalType { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardsDataVip
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("due_date")]
+ public long DueDate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_pay_type")]
+ public int VipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("theme_type")]
+ public int ThemeType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label")]
+ public UserCardsDataVipLabel Label { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript")]
+ public int AvatarSubscript { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nickname_color")]
+ public string NicknameColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript_url")]
+ public string AvatarSubscriptUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_status")]
+ public int TvVipStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_pay_type")]
+ public int TvVipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_due_date")]
+ public long TvDueDate { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardsDataVipLabel
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label_theme")]
+ public string LabelTheme { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_color")]
+ public string TextColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_style")]
+ public int BgStyle { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_color")]
+ public string BgColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("border_color")]
+ public string BorderColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("use_img_label")]
+ public bool UseImgLabel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans")]
+ public string ImgLabelUriHans { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant")]
+ public string ImgLabelUriHant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans_static")]
+ public string ImgLabelUriHansStatic { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant_static")]
+ public string ImgLabelUriHantStatic { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardsDataPendant
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("pid")]
+ public int Pid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("expire")]
+ public int Expire { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance")]
+ public string ImageEnhance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance_frame")]
+ public string ImageEnhanceFrame { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardsDataNameplate
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("nid")]
+ public int Nid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_small")]
+ public string ImageSmall { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public string Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("condition")]
+ public string Condition { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserCardsDataOfficial
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Models/User/UserSpaceInfo.cs b/src/BiliSharp/Api/Models/User/UserSpaceInfo.cs
new file mode 100644
index 0000000..9cdd58b
--- /dev/null
+++ b/src/BiliSharp/Api/Models/User/UserSpaceInfo.cs
@@ -0,0 +1,827 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace BiliSharp.Api.Models.User
+{
+ ///
+ /// https://api.bilibili.com/x/space/acc/info?mid=
+ ///
+ public class UserSpaceInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("ttl")]
+ public int Ttl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("data")]
+ public UserSpaceInfoData Data { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoData
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public long Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sex")]
+ public string Sex { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face")]
+ public string Face { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft")]
+ public int FaceNft { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("face_nft_type")]
+ public int FaceNftType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sign")]
+ public string Sign { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("rank")]
+ public int Rank { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public int Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("jointime")]
+ public int Jointime { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("moral")]
+ public int Moral { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("silence")]
+ public int Silence { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("coins")]
+ public int Coins { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("fans_badge")]
+ public bool FansBadge { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("fans_medal")]
+ public UserSpaceInfoDataFansMedal FansMedal { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("official")]
+ public UserSpaceInfoDataOfficial Official { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip")]
+ public UserSpaceInfoDataVip Vip { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("pendant")]
+ public UserSpaceInfoDataPendant Pendant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nameplate")]
+ public UserSpaceInfoDataNameplate Nameplate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("user_honour_info")]
+ public UserSpaceInfoDataUserHonourInfo UserHonourInfo { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_followed")]
+ public bool IsFollowed { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("top_photo")]
+ public string TopPhoto { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("theme")]
+ public UserSpaceInfoDataTheme Theme { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("sys_notice")]
+ public UserSpaceInfoDataSysNotice SysNotice { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("live_room")]
+ public UserSpaceInfoDataLiveRoom LiveRoom { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("birthday")]
+ public string Birthday { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("school")]
+ public UserSpaceInfoDataSchool School { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("profession")]
+ public UserSpaceInfoDataProfession Profession { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tags")]
+ public object Tags { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("series")]
+ public UserSpaceInfoDataSeries Series { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_senior_member")]
+ public int IsSeniorMember { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("mcn_info")]
+ public object McnInfo { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("gaia_res_type")]
+ public int GaiaResType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("gaia_data")]
+ public object GaiaData { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_risk")]
+ public bool IsRisk { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("elec")]
+ public UserSpaceInfoDataElec Elec { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("contract")]
+ public UserSpaceInfoDataContract Contract { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataFansMedal
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("show")]
+ public bool Show { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("wear")]
+ public bool Wear { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("medal")]
+ public object Medal { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataOfficial
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("desc")]
+ public string Desc { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataVip
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("type")]
+ public int Type { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("status")]
+ public int Status { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("due_date")]
+ public long DueDate { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("vip_pay_type")]
+ public int VipPayType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("theme_type")]
+ public int ThemeType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label")]
+ public UserSpaceInfoDataVipLabel Label { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript")]
+ public int AvatarSubscript { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("nickname_color")]
+ public string NicknameColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("role")]
+ public int Role { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("avatar_subscript_url")]
+ public string AvatarSubscriptUrl { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_status")]
+ public int TvVipStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tv_vip_pay_type")]
+ public int TvVipPayType { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataVipLabel
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("label_theme")]
+ public string LabelTheme { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_color")]
+ public string TextColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_style")]
+ public int BgStyle { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("bg_color")]
+ public string BgColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("border_color")]
+ public string BorderColor { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("use_img_label")]
+ public bool UseImgLabel { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans")]
+ public string ImgLabelUriHans { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant")]
+ public string ImgLabelUriHant { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hans_static")]
+ public string ImgLabelUriHansStatic { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("img_label_uri_hant_static")]
+ public string ImgLabelUriHantStatic { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataPendant
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("pid")]
+ public int Pid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("expire")]
+ public int Expire { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance")]
+ public string ImageEnhance { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_enhance_frame")]
+ public string ImageEnhanceFrame { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataNameplate
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("nid")]
+ public int Nid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("image_small")]
+ public string ImageSmall { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("level")]
+ public string Level { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("condition")]
+ public string Condition { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataUserHonourInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("mid")]
+ public int Mid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("colour")]
+ public object Colour { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("tags")]
+ public List Tags { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataTheme
+ { }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataSysNotice
+ { }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataLiveRoom
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("roomStatus")]
+ public int Roomstatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("liveStatus")]
+ public int Livestatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("url")]
+ public string Url { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("cover")]
+ public string Cover { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("roomid")]
+ public long Roomid { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("roundStatus")]
+ public int Roundstatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("broadcast_type")]
+ public int BroadcastType { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("watched_show")]
+ public UserSpaceInfoDataLiveRoomWatchedShow WatchedShow { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataLiveRoomWatchedShow
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("switch")]
+ public bool Switch { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("num")]
+ public int Num { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_small")]
+ public string TextSmall { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("text_large")]
+ public string TextLarge { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("icon")]
+ public string Icon { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("icon_location")]
+ public string IconLocation { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("icon_web")]
+ public string IconWeb { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataSchool
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataProfession
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("department")]
+ public string Department { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_show")]
+ public int IsShow { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataSeries
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("user_upgrade_status")]
+ public int UserUpgradeStatus { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("show_upgrade_window")]
+ public bool ShowUpgradeWindow { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataElec
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("show_info")]
+ public UserSpaceInfoDataElecShowInfo ShowInfo { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataElecShowInfo
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("show")]
+ public bool Show { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("state")]
+ public int State { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("icon")]
+ public string Icon { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("jump_url")]
+ public string JumpUrl { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class UserSpaceInfoDataContract
+ {
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_display")]
+ public bool IsDisplay { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonPropertyName("is_follow_display")]
+ public bool IsFollowDisplay { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/Sign/WbiSign.cs b/src/BiliSharp/Api/Sign/WbiSign.cs
new file mode 100644
index 0000000..43e4937
--- /dev/null
+++ b/src/BiliSharp/Api/Sign/WbiSign.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace BiliSharp.Api.Sign
+{
+ public static class WbiSign
+ {
+ private static Tuple Keys;
+
+ ///
+ /// 打乱重排实时口令
+ ///
+ ///
+ ///
+ private static string GetMixinKey(string origin)
+ {
+ int[] mixinKeyEncTab = new int[]
+ {
+ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
+ 33, 9, 42, 19, 29, 28, 14, 39,12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
+ 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
+ 36, 20, 34, 44, 52
+ };
+
+ var temp = new StringBuilder();
+ foreach (var i in mixinKeyEncTab)
+ {
+ temp.Append(origin[i]);
+ }
+ return temp.ToString()[..32];
+ }
+
+ ///
+ /// 将字典参数转为字符串
+ ///
+ ///
+ ///
+ public static string ParametersToQuery(Dictionary parameters)
+ {
+ var keys = parameters.Keys.ToList();
+ var queryList = new List();
+ foreach (var item in keys)
+ {
+ var value = parameters[item];
+ queryList.Add($"{item}={value}");
+ }
+ return string.Join("&", queryList);
+ }
+
+ ///
+ /// 返回imgKey,subKey
+ ///
+ ///
+ public static Tuple GetKey()
+ {
+ // 当Keys为null时,返回一个硬编码的key,
+ // 这个key无效,只为了保证不报错
+ return Keys ?? new Tuple("653657f524a547ac981ded72ea172057", "6e4909c702f846728e64f6007736a338");
+ }
+
+ ///
+ /// 设置imgKey,subKey
+ ///
+ ///
+ public static void SetKey(Tuple keys)
+ {
+ Keys = keys;
+ }
+
+ ///
+ /// Wbi签名,返回所有参数字典
+ ///
+ ///
+ ///
+ public static Dictionary EncodeWbi(Dictionary parameters)
+ {
+ return EncodeWbi(parameters, GetKey().Item1, GetKey().Item2);
+ }
+
+ ///
+ /// Wbi签名,返回所有参数字典
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Dictionary EncodeWbi(Dictionary parameters, string imgKey, string subKey)
+ {
+ var mixinKey = GetMixinKey(imgKey + subKey);
+
+ var chrFilter = new Regex("[!'()*]");
+
+ var newParameters = new Dictionary
+ {
+ { "wts", (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds }
+ };
+
+ foreach (var para in parameters)
+ {
+ var key = para.Key;
+ var value = para.Value.ToString();
+
+ var encodedValue = chrFilter.Replace(value, "");
+
+ newParameters.Add(Uri.EscapeDataString(key), Uri.EscapeDataString(encodedValue));
+ }
+
+ var keys = newParameters.Keys.ToList();
+ keys.Sort();
+
+ var queryList = new List();
+ foreach (var item in keys)
+ {
+ var value = newParameters[item];
+ queryList.Add($"{item}={value}");
+ }
+
+ var queryString = string.Join("&", queryList);
+ var md5Hasher = MD5.Create();
+ var hashStr = queryString + mixinKey;
+ var hashedQueryString = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(hashStr));
+ var wbiSign = BitConverter.ToString(hashedQueryString).Replace("-", "").ToLower();
+
+ newParameters.Add("w_rid", wbiSign);
+ return newParameters;
+ }
+
+ }
+}
diff --git a/src/BiliSharp/Api/User/Nickname.cs b/src/BiliSharp/Api/User/Nickname.cs
new file mode 100644
index 0000000..ddcab9e
--- /dev/null
+++ b/src/BiliSharp/Api/User/Nickname.cs
@@ -0,0 +1,19 @@
+namespace BiliSharp.Api.User
+{
+ ///
+ /// 检查昵称是否可注册
+ ///
+ public static class Nickname
+ {
+ ///
+ /// 检查昵称
+ ///
+ ///
+ ///
+ public static Models.User.Nickname CheckNickname(string nickname)
+ {
+ string url = $"https://passport.bilibili.com/web/generic/check/nickname?nickName={nickname}";
+ return Utils.GetData(url);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/Api/User/UserInfo.cs b/src/BiliSharp/Api/User/UserInfo.cs
new file mode 100644
index 0000000..b89f3e2
--- /dev/null
+++ b/src/BiliSharp/Api/User/UserInfo.cs
@@ -0,0 +1,66 @@
+using BiliSharp.Api.Models.User;
+using BiliSharp.Api.Sign;
+using System.Collections.Generic;
+
+namespace BiliSharp.Api.User
+{
+ ///
+ /// 用户基本信息
+ ///
+ public static class UserInfo
+ {
+ ///
+ /// 用户空间详细信息
+ ///
+ ///
+ public static UserSpaceInfo GetUserInfo(long mid)
+ {
+ var parameters = new Dictionary
+ {
+ { "mid", mid }
+ };
+ string query = WbiSign.ParametersToQuery(WbiSign.EncodeWbi(parameters));
+ string url = $"https://api.bilibili.com/x/space/wbi/acc/info?{query}";
+ return Utils.GetData(url);
+ }
+
+ ///
+ /// 用户名片信息
+ ///
+ ///
+ ///
+ ///
+ public static UserCard GetUserCard(long mid, bool isPhoto = false)
+ {
+ string url = $"https://api.bilibili.com/x/web-interface/card?mid={mid}&photo={isPhoto}";
+ return Utils.GetData(url);
+ }
+
+ ///
+ /// 登录用户空间详细信息
+ ///
+ ///
+ public static MyInfo GetMyInfo()
+ {
+ string url = "https://api.bilibili.com/x/space/myinfo";
+ return Utils.GetData(url);
+ }
+
+ ///
+ /// 多用户详细信息
+ ///
+ ///
+ ///
+ public static UserCards GetUserCards(List ids)
+ {
+ string url = "https://api.vc.bilibili.com/account/v1/user/cards?uids=";
+ foreach (long id in ids)
+ {
+ url += $"{id},";
+ }
+ url = url.TrimEnd(',');
+
+ return Utils.GetData(url);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/BiliManager.cs b/src/BiliSharp/BiliManager.cs
new file mode 100644
index 0000000..2aaabf2
--- /dev/null
+++ b/src/BiliSharp/BiliManager.cs
@@ -0,0 +1,53 @@
+using System.Net;
+
+namespace BiliSharp
+{
+ public class BiliManager
+ {
+ private static readonly BiliManager _instance = new BiliManager();
+ private string _userAgent;
+ private CookieContainer _cookies;
+
+ private BiliManager() { }
+
+ public static BiliManager Instance()
+ {
+ return _instance;
+ }
+
+ ///
+ /// 设置cookies
+ ///
+ ///
+ ///
+ public BiliManager SetCookies(CookieContainer cookies)
+ {
+ _cookies = cookies;
+ return this;
+ }
+
+ ///
+ /// 获取cookies
+ ///
+ ///
+ public CookieContainer GetCookies() { return _cookies; }
+
+ ///
+ /// 设置userAgent
+ ///
+ ///
+ ///
+ public BiliManager SetUserAgent(string userAgent)
+ {
+ _userAgent = userAgent;
+ return this;
+ }
+
+ ///
+ /// 获取userAgent
+ ///
+ ///
+ public string GetUserAgent() { return _userAgent; }
+
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/BiliSharp.csproj b/src/BiliSharp/BiliSharp.csproj
new file mode 100644
index 0000000..c675825
--- /dev/null
+++ b/src/BiliSharp/BiliSharp.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.1
+
+
+
+
+
+
+
diff --git a/src/BiliSharp/Utils.cs b/src/BiliSharp/Utils.cs
new file mode 100644
index 0000000..7ff2142
--- /dev/null
+++ b/src/BiliSharp/Utils.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Net;
+using System.Text.Json;
+
+namespace BiliSharp
+{
+ internal static class Utils
+ {
+ internal static T GetData(string url)
+ {
+ string referer = "https://www.bilibili.com";
+ string userAgent = BiliManager.Instance().GetUserAgent();
+ CookieContainer cookies = BiliManager.Instance().GetCookies();
+
+ string response = WebClient.RequestWeb(url, referer, userAgent, cookies);
+
+ try
+ {
+ var obj = JsonSerializer.Deserialize(response);
+ return obj;
+ }
+ catch (Exception)
+ {
+ return default;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/BiliSharp/WebClient.cs b/src/BiliSharp/WebClient.cs
new file mode 100644
index 0000000..9d8a0c5
--- /dev/null
+++ b/src/BiliSharp/WebClient.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Net;
+using System.Text;
+
+namespace BiliSharp
+{
+ internal static class WebClient
+ {
+ ///
+ /// 发送get或post请求
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static string RequestWeb(string url, string referer = null, string userAgent = null, CookieContainer cookies = null, string method = "GET", Dictionary parameters = null, int retry = 3)
+ {
+ // 重试次数
+ if (retry <= 0) { return ""; }
+
+ // post请求,发送参数
+ if (method == "POST" && parameters != null)
+ {
+ var builder = new StringBuilder();
+ int i = 0;
+ foreach (var item in parameters)
+ {
+ if (i > 0)
+ {
+ builder.Append('&');
+ }
+
+ builder.AppendFormat("{0}={1}", item.Key, item.Value);
+ i++;
+ }
+
+ url += "?" + builder.ToString();
+ }
+
+ try
+ {
+ HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
+ request.Method = method;
+ request.Timeout = 60 * 1000;
+
+ request.ContentType = "application/json,text/html,application/xhtml+xml,application/xml;charset=UTF-8";
+ request.Headers["accept-language"] = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7";
+ request.Headers["accept-encoding"] = "gzip, deflate, br";
+
+ // userAgent
+ if (userAgent != null)
+ {
+ request.UserAgent = userAgent;
+ }
+ else
+ {
+ request.UserAgent = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36";
+ }
+
+ // referer
+ if (referer != null)
+ {
+ request.Referer = referer;
+ }
+
+ // 构造cookie
+ if (!url.Contains("getLogin"))
+ {
+ //request.Headers["origin"] = "https://www.bilibili.com";
+
+ if (cookies != null)
+ {
+ request.CookieContainer = cookies;
+ }
+ }
+
+ string html = string.Empty;
+ using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
+ {
+ if (response.ContentEncoding.ToLower().Contains("gzip"))
+ {
+ using var stream = new GZipStream(response.GetResponseStream(), CompressionMode.Decompress);
+ using var reader = new StreamReader(stream, Encoding.UTF8);
+ html = reader.ReadToEnd();
+ }
+ else if (response.ContentEncoding.ToLower().Contains("deflate"))
+ {
+ using var stream = new DeflateStream(response.GetResponseStream(), CompressionMode.Decompress);
+ using var reader = new StreamReader(stream, Encoding.UTF8);
+ html = reader.ReadToEnd();
+ }
+ else if (response.ContentEncoding.ToLower().Contains("br"))
+ {
+ using var stream = new BrotliStream(response.GetResponseStream(), CompressionMode.Decompress);
+ using var reader = new StreamReader(stream, Encoding.UTF8);
+ html = reader.ReadToEnd();
+ }
+ else
+ {
+ using var stream = response.GetResponseStream();
+ using var reader = new StreamReader(stream, Encoding.UTF8);
+ html = reader.ReadToEnd();
+ }
+ }
+
+ return html;
+ }
+ catch (Exception)
+ {
+ return RequestWeb(url, referer, userAgent, cookies, method, parameters, retry - 1);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/BiliLocator.cs b/src/Downkyi.Core/Bili/BiliLocator.cs
new file mode 100644
index 0000000..4464eae
--- /dev/null
+++ b/src/Downkyi.Core/Bili/BiliLocator.cs
@@ -0,0 +1,24 @@
+namespace Downkyi.Core.Bili;
+
+public static class BiliLocator
+{
+ private static ILogin _login;
+ public static ILogin Login
+ {
+ get
+ {
+ _login ??= new Web.Login();
+ return _login;
+ }
+ }
+
+ private static IUser _user;
+ public static IUser User
+ {
+ get
+ {
+ _user ??= new Web.User();
+ return _user;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Cookies.cs b/src/Downkyi.Core/Bili/Cookies.cs
new file mode 100644
index 0000000..42f46b2
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Cookies.cs
@@ -0,0 +1,144 @@
+using Downkyi.Core.Utils;
+using System.Collections;
+using System.Net;
+using System.Web;
+
+namespace Downkyi.Core.Bili;
+
+public static class Cookies
+{
+ ///
+ /// 写入cookies到磁盘
+ ///
+ ///
+ ///
+ ///
+ public static bool WriteCookiesToDisk(string file, CookieContainer cookieJar)
+ {
+ return ObjectHelper.WriteObjectToDisk(file, cookieJar);
+ }
+
+ ///
+ /// 从磁盘读取cookie
+ ///
+ ///
+ ///
+ public static CookieContainer ReadCookiesFromDisk(string file)
+ {
+ return (CookieContainer)ObjectHelper.ReadObjectFromDisk(file);
+ }
+
+ ///
+ /// 将CookieContainer中的所有的Cookie读出来
+ ///
+ ///
+ ///
+ public static List GetAllCookies(CookieContainer cc)
+ {
+ var lstCookies = new List();
+
+ Hashtable table = (Hashtable)cc.GetType().InvokeMember("m_domainTable",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField |
+ System.Reflection.BindingFlags.Instance, null, cc, Array.Empty());
+
+ foreach (object pathList in table.Values)
+ {
+ SortedList lstCookieCol = (SortedList)pathList.GetType().InvokeMember("m_list",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField
+ | System.Reflection.BindingFlags.Instance, null, pathList, Array.Empty());
+ foreach (CookieCollection colCookies in lstCookieCol.Values)
+ {
+ foreach (Cookie c in colCookies.Cast())
+ {
+ lstCookies.Add(c);
+ }
+ }
+ }
+
+ return lstCookies;
+ }
+
+ ///
+ /// 返回cookies的字符串
+ ///
+ ///
+ public static string GetCookiesString(CookieContainer cc)
+ {
+ var cookies = GetAllCookies(cc);
+
+ string cookie = string.Empty;
+ foreach (var item in cookies)
+ {
+ cookie += item.ToString() + ";";
+ }
+ return cookie.TrimEnd(';');
+ }
+
+ ///
+ /// 解析二维码登录返回的url,用于设置cookie
+ ///
+ ///
+ ///
+ public static CookieContainer ParseCookieByUrl(string url)
+ {
+ var cookieContainer = new CookieContainer();
+
+ if (url == null || url == "") { return cookieContainer; }
+
+ string[] strList = url.Split('?');
+ if (strList.Length < 2) { return cookieContainer; }
+
+ string[] strList2 = strList[1].Split('&');
+ if (strList2.Length == 0) { return cookieContainer; }
+
+ // 获取expires
+ string expires = strList2.FirstOrDefault(it => it.Contains("Expires")).Split('=')[1];
+ DateTime dateTime = DateTime.Now;
+ dateTime = dateTime.AddSeconds(int.Parse(expires));
+
+ foreach (var item in strList2)
+ {
+ string[] strList3 = item.Split('=');
+ if (strList3.Length < 2) { continue; }
+
+ string name = strList3[0];
+ string value = strList3[1];
+ value = HttpUtility.UrlEncode(value);
+
+ // 不需要
+ if (name == "Expires" || name == "gourl") { continue; }
+
+ // 添加cookie
+ cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime });
+
+ }
+
+ return cookieContainer;
+ }
+
+ ///
+ /// 解析从浏览器获取的cookies,用于设置cookie
+ ///
+ ///
+ ///
+ public static CookieContainer ParseCookieByString(string str)
+ {
+ var cookieContainer = new CookieContainer();
+
+ var cookies = str.Replace(" ", "").Split(";");
+ foreach (var cookie in cookies)
+ {
+ DateTime dateTime = DateTime.Now;
+ dateTime = dateTime.AddMonths(12);
+
+ var temp = cookie.Split("=");
+ var name = temp[0];
+ var value = temp[1];
+
+ // 添加cookie
+ cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime });
+ }
+ return cookieContainer;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/ILogin.cs b/src/Downkyi.Core/Bili/ILogin.cs
new file mode 100644
index 0000000..7cf6a6a
--- /dev/null
+++ b/src/Downkyi.Core/Bili/ILogin.cs
@@ -0,0 +1,25 @@
+using Downkyi.Core.Bili.Models;
+
+namespace Downkyi.Core.Bili;
+
+public interface ILogin
+{
+ ///
+ /// 申请二维码
+ ///
+ /// (url, key)
+ Tuple GetQRCodeUrl();
+
+ ///
+ /// 扫码登录
+ ///
+ ///
+ ///
+ QRCodeStatus PollQRCode(string qrcodeKey);
+
+ ///
+ /// 导航栏用户信息
+ ///
+ ///
+ NavigationInfo GetNavigationInfo();
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/IUser.cs b/src/Downkyi.Core/Bili/IUser.cs
new file mode 100644
index 0000000..e98f65d
--- /dev/null
+++ b/src/Downkyi.Core/Bili/IUser.cs
@@ -0,0 +1,5 @@
+namespace Downkyi.Core.Bili;
+
+public interface IUser
+{
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Models/NavigationInfo.cs b/src/Downkyi.Core/Bili/Models/NavigationInfo.cs
new file mode 100644
index 0000000..927ebc2
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Models/NavigationInfo.cs
@@ -0,0 +1,10 @@
+namespace Downkyi.Core.Bili.Models;
+
+public class NavigationInfo
+{
+ public long Mid { get; set; }
+ public string Name { get; set; }
+ public string Header { get; set; }
+ public int VipStatus { get; set; } // 会员开通状态 // 0:无;1:有
+ public bool IsLogin { get; set; }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Models/QRCodeStatus.cs b/src/Downkyi.Core/Bili/Models/QRCodeStatus.cs
new file mode 100644
index 0000000..05f4e27
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Models/QRCodeStatus.cs
@@ -0,0 +1,17 @@
+namespace Downkyi.Core.Bili.Models;
+
+public class QRCodeStatus
+{
+ public string Url { get; set; }
+ public string Token { get; set; }
+ public long Timestamp { get; set; }
+
+ ///
+ /// 0:扫码登录成功
+ /// 86038:二维码已失效
+ /// 86090:二维码已扫码未确认
+ /// 86101:未扫码
+ ///
+ public long Code { get; set; }
+ public string Message { get; set; }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Models/Quality.cs b/src/Downkyi.Core/Bili/Models/Quality.cs
new file mode 100644
index 0000000..aad41c6
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Models/Quality.cs
@@ -0,0 +1,7 @@
+namespace Downkyi.Core.Bili.Models;
+
+public class Quality
+{
+ public string Name { get; set; }
+ public int Id { get; set; }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Utils/BvId.cs b/src/Downkyi.Core/Bili/Utils/BvId.cs
new file mode 100644
index 0000000..4951631
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Utils/BvId.cs
@@ -0,0 +1,60 @@
+namespace Downkyi.Core.Bili.Utils;
+
+public static class BvId
+{
+ private const string tableStr = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"; //码表
+ private static readonly char[] table = tableStr.ToCharArray();
+
+ private static readonly char[] tr = new char[124]; //反查码表
+ private const ulong Xor = 177451812; //固定异或值
+ private const ulong add = 8728348608; //固定加法值
+ private static readonly int[] s = { 11, 10, 3, 8, 4, 6 }; //位置编码表
+
+ static BvId()
+ {
+ Tr_init();
+ }
+
+ //初始化反查码表
+ private static void Tr_init()
+ {
+ for (int i = 0; i < 58; i++)
+ tr[table[i]] = (char)i;
+ }
+
+ ///
+ /// bvid转avid
+ ///
+ ///
+ ///
+ public static ulong Bv2Av(string bvid)
+ {
+ char[] bv = bvid.ToCharArray();
+
+ ulong r = 0;
+ ulong av;
+ for (int i = 0; i < 6; i++)
+ r += tr[bv[s[i]]] * (ulong)Math.Pow(58, i);
+ av = (r - add) ^ Xor;
+ return av;
+ }
+
+ ///
+ /// avid转bvid
+ ///
+ ///
+ ///
+ public static string Av2Bv(ulong av)
+ {
+ //编码结果
+ string res = "BV1 4 1 7 ";
+ char[] result = res.ToCharArray();
+
+ av = (av ^ Xor) + add;
+ for (int i = 0; i < 6; i++)
+ result[s[i]] = table[av / (ulong)Math.Pow(58, i) % 58];
+ var bv = new string(result);
+ return bv;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Utils/DanmakuSender.cs b/src/Downkyi.Core/Bili/Utils/DanmakuSender.cs
new file mode 100644
index 0000000..4cffd80
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Utils/DanmakuSender.cs
@@ -0,0 +1,151 @@
+namespace Downkyi.Core.Bili.Utils;
+
+public static class DanmakuSender
+{
+ private const uint CRCPOLYNOMIAL = 0xEDB88320;
+ private static readonly uint[] crctable = new uint[256];
+
+ static DanmakuSender()
+ {
+ CreateTable();
+ }
+
+ private static void CreateTable()
+ {
+ for (int i = 0; i < 256; i++)
+ {
+ uint crcreg = (uint)i;
+
+ for (int j = 0; j < 8; j++)
+ {
+ if ((crcreg & 1) != 0)
+ {
+ crcreg = CRCPOLYNOMIAL ^ (crcreg >> 1);
+ }
+ else
+ {
+ crcreg >>= 1;
+ }
+ }
+ crctable[i] = crcreg;
+ }
+ }
+
+ private static uint Crc32(string userId)
+ {
+ uint crcstart = 0xFFFFFFFF;
+ for (int i = 0; i < userId.Length; i++)
+ {
+ uint index = (uint)(crcstart ^ (int)userId[i]) & 255;
+ crcstart = (crcstart >> 8) ^ crctable[index];
+ }
+ return crcstart;
+ }
+
+ private static uint Crc32LastIndex(string userId)
+ {
+ uint index = 0;
+ uint crcstart = 0xFFFFFFFF;
+ for (int i = 0; i < userId.Length; i++)
+ {
+ index = (uint)((crcstart ^ (int)userId[i]) & 255);
+ crcstart = (crcstart >> 8) ^ crctable[index];
+ }
+ return index;
+ }
+
+ private static int GetCrcIndex(long t)
+ {
+ for (int i = 0; i < 256; i++)
+ {
+ if ((crctable[i] >> 24) == t)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static object[] DeepCheck(int i, int[] index)
+ {
+ object[] resultArray = new object[2];
+
+ string result = "";
+ uint tc;// = 0x00;
+ var hashcode = Crc32(i.ToString());
+ tc = (uint)(hashcode & 0xff ^ index[2]);
+
+ if (!(tc <= 57 && tc >= 48))
+ {
+ resultArray[0] = 0;
+ return resultArray;
+ }
+
+ result += (tc - 48).ToString();
+ hashcode = crctable[index[2]] ^ (hashcode >> 8);
+ tc = (uint)(hashcode & 0xff ^ index[1]);
+
+ if (!(tc <= 57 && tc >= 48))
+ {
+ resultArray[0] = 0;
+ return resultArray;
+ }
+
+ result += (tc - 48).ToString();
+ hashcode = crctable[index[1]] ^ (hashcode >> 8);
+ tc = (uint)(hashcode & 0xff ^ index[0]);
+
+ if (!(tc <= 57 && tc >= 48))
+ {
+ resultArray[0] = 0;
+ return resultArray;
+ }
+
+ result += (tc - 48).ToString();
+ //hashcode = crctable[index[0]] ^ (hashcode >> 8);
+
+ resultArray[0] = 1;
+ resultArray[1] = result;
+ return resultArray;
+ }
+
+ ///
+ /// 查询弹幕发送者
+ ///
+ ///
+ ///
+ public static string FindDanmakuSender(string userId)
+ {
+ object[] deepCheckData = new object[2];
+
+ int[] index = new int[4];
+ uint ht = (uint)Convert.ToInt32($"0x{userId}", 16);
+ ht ^= 0xffffffff;
+
+ int i;
+ for (i = 3; i > -1; i--)
+ {
+ index[3 - i] = GetCrcIndex(ht >> (i * 8));
+ uint snum = crctable[index[3 - i]];
+ ht ^= snum >> ((3 - i) * 8);
+ }
+ for (i = 0; i < 100000000; i++)
+ {
+ uint lastindex = Crc32LastIndex(i.ToString());
+ if (lastindex == index[3])
+ {
+ deepCheckData = DeepCheck(i, index);
+ if ((int)deepCheckData[0] != 0)
+ {
+ break;
+ }
+ }
+ }
+ if (i == 100000000)
+ {
+ return "-1";
+ }
+ return $"{i}{deepCheckData[1]}";
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Utils/ParseEntrance.cs b/src/Downkyi.Core/Bili/Utils/ParseEntrance.cs
new file mode 100644
index 0000000..4ad1f93
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Utils/ParseEntrance.cs
@@ -0,0 +1,556 @@
+using Downkyi.Core.Utils.Validator;
+using System.Text.RegularExpressions;
+
+namespace Downkyi.Core.Bili.Utils;
+
+///
+/// 解析输入的字符串
+/// 支持的格式有:
+/// av号:av170001, AV170001, https://www.bilibili.com/video/av170001
+/// BV号:BV17x411w7KC, https://www.bilibili.com/video/BV17x411w7KC, https://b23.tv/BV17x411w7KC
+/// 番剧(电影、电视剧)ss号:ss32982, SS32982, https://www.bilibili.com/bangumi/play/ss32982
+/// 番剧(电影、电视剧)ep号:ep317925, EP317925, https://www.bilibili.com/bangumi/play/ep317925
+/// 番剧(电影、电视剧)md号:md28228367, MD28228367, https://www.bilibili.com/bangumi/media/md28228367
+/// 课程ss号:https://www.bilibili.com/cheese/play/ss205
+/// 课程ep号:https://www.bilibili.com/cheese/play/ep3489
+/// 收藏夹:ml1329019876, ML1329019876, https://www.bilibili.com/medialist/detail/ml1329019876, https://www.bilibili.com/medialist/play/ml1329019876/
+/// 用户空间:uid928123, UID928123, uid:928123, UID:928123, https://space.bilibili.com/928123
+///
+public static class ParseEntrance
+{
+ public static readonly string WwwUrl = "https://www.bilibili.com";
+ public static readonly string ShareWwwUrl = "https://www.bilibili.com/s";
+ public static readonly string ShortUrl = "https://b23.tv/";
+ public static readonly string MobileUrl = "https://m.bilibili.com";
+
+ public static readonly string SpaceUrl = "https://space.bilibili.com";
+
+ public static readonly string VideoUrl = $"{WwwUrl}/video/";
+ public static readonly string BangumiUrl = $"{WwwUrl}/bangumi/play/";
+ public static readonly string BangumiMediaUrl = $"{WwwUrl}/bangumi/media/";
+ public static readonly string CheeseUrl = $"{WwwUrl}/cheese/play/";
+ public static readonly string FavoritesUrl1 = $"{WwwUrl}/medialist/detail/";
+ public static readonly string FavoritesUrl2 = $"{WwwUrl}/medialist/play/";
+
+ #region 视频
+
+ ///
+ /// 是否为av id
+ ///
+ ///
+ ///
+ public static bool IsAvId(string input)
+ {
+ return IsIntId(input, "av");
+ }
+
+ ///
+ /// 是否为av url
+ ///
+ ///
+ ///
+ public static bool IsAvUrl(string input)
+ {
+ string id = GetVideoId(input);
+ return IsAvId(id);
+ }
+
+ ///
+ /// 获取av id
+ ///
+ ///
+ ///
+ public static long GetAvId(string input)
+ {
+ if (IsAvId(input))
+ {
+ return Number.GetInt(input.Remove(0, 2));
+ }
+ else if (IsAvUrl(input))
+ {
+ return Number.GetInt(GetVideoId(input).Remove(0, 2));
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ ///
+ /// 是否为bv id
+ ///
+ ///
+ ///
+ public static bool IsBvId(string input)
+ {
+ return input.StartsWith("BV") && input.Length == 12;
+ }
+
+ ///
+ /// 是否为bv url
+ ///
+ ///
+ ///
+ public static bool IsBvUrl(string input)
+ {
+ string id = GetVideoId(input);
+ return IsBvId(id);
+ }
+
+ ///
+ /// 获取bv id
+ ///
+ ///
+ ///
+ public static string GetBvId(string input)
+ {
+ if (IsBvId(input))
+ {
+ return input;
+ }
+ else if (IsBvUrl(input))
+ {
+ return GetVideoId(input);
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ #endregion
+
+ #region 番剧(电影、电视剧)
+
+ ///
+ /// 是否为番剧season id
+ ///
+ ///
+ ///
+ public static bool IsBangumiSeasonId(string input)
+ {
+ return IsIntId(input, "ss");
+ }
+
+ ///
+ /// 是否为番剧season url
+ ///
+ ///
+ ///
+ public static bool IsBangumiSeasonUrl(string input)
+ {
+ string id = GetBangumiId(input);
+ return IsBangumiSeasonId(id);
+ }
+
+ ///
+ /// 获取番剧season id
+ ///
+ ///
+ ///
+ public static long GetBangumiSeasonId(string input)
+ {
+ if (IsBangumiSeasonId(input))
+ {
+ return Number.GetInt(input.Remove(0, 2));
+ }
+ else if (IsBangumiSeasonUrl(input))
+ {
+ return Number.GetInt(GetBangumiId(input).Remove(0, 2));
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ ///
+ /// 是否为番剧episode id
+ ///
+ ///
+ ///
+ public static bool IsBangumiEpisodeId(string input)
+ {
+ return IsIntId(input, "ep");
+ }
+
+ ///
+ /// 是否为番剧episode url
+ ///
+ ///
+ ///
+ public static bool IsBangumiEpisodeUrl(string input)
+ {
+ string id = GetBangumiId(input);
+ return IsBangumiEpisodeId(id);
+ }
+
+ ///
+ /// 获取番剧episode id
+ ///
+ ///
+ ///
+ public static long GetBangumiEpisodeId(string input)
+ {
+ if (IsBangumiEpisodeId(input))
+ {
+ return Number.GetInt(input.Remove(0, 2));
+ }
+ else if (IsBangumiEpisodeUrl(input))
+ {
+ return Number.GetInt(GetBangumiId(input).Remove(0, 2));
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ ///
+ /// 是否为番剧media id
+ ///
+ ///
+ ///
+ public static bool IsBangumiMediaId(string input)
+ {
+ return IsIntId(input, "md");
+ }
+
+ ///
+ /// 是否为番剧media url
+ ///
+ ///
+ ///
+ public static bool IsBangumiMediaUrl(string input)
+ {
+ string id = GetBangumiId(input);
+ return IsBangumiMediaId(id);
+ }
+
+ ///
+ /// 获取番剧media id
+ ///
+ ///
+ ///
+ public static long GetBangumiMediaId(string input)
+ {
+ if (IsBangumiMediaId(input))
+ {
+ return Number.GetInt(input.Remove(0, 2));
+ }
+ else if (IsBangumiMediaUrl(input))
+ {
+ return Number.GetInt(GetBangumiId(input).Remove(0, 2));
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ #endregion
+
+ #region 课程
+
+ ///
+ /// 是否为课程season url
+ ///
+ ///
+ ///
+ public static bool IsCheeseSeasonUrl(string input)
+ {
+ string id = GetCheeseId(input);
+ return IsIntId(id, "ss");
+ }
+
+ ///
+ /// 获取课程season id
+ ///
+ ///
+ ///
+ public static long GetCheeseSeasonId(string input)
+ {
+ return IsCheeseSeasonUrl(input) ? Number.GetInt(GetCheeseId(input).Remove(0, 2)) : -1;
+ }
+
+ ///
+ /// 是否为课程episode url
+ ///
+ ///
+ ///
+ public static bool IsCheeseEpisodeUrl(string input)
+ {
+ string id = GetCheeseId(input);
+ return IsIntId(id, "ep");
+ }
+
+ ///
+ /// 获取课程episode id
+ ///
+ ///
+ ///
+ public static long GetCheeseEpisodeId(string input)
+ {
+ return IsCheeseEpisodeUrl(input) ? Number.GetInt(GetCheeseId(input).Remove(0, 2)) : -1;
+ }
+
+ #endregion
+
+ #region 收藏夹
+
+ ///
+ /// 是否为收藏夹id
+ ///
+ ///
+ ///
+ public static bool IsFavoritesId(string input)
+ {
+ return IsIntId(input, "ml");
+ }
+
+ ///
+ /// 是否为收藏夹url
+ ///
+ ///
+ ///
+ public static bool IsFavoritesUrl(string input)
+ {
+ return IsFavoritesUrl1(input) || IsFavoritesUrl2(input);
+ }
+
+ ///
+ /// 是否为收藏夹url1
+ ///
+ ///
+ ///
+ private static bool IsFavoritesUrl1(string input)
+ {
+ string favoritesId = GetId(input, FavoritesUrl1);
+ return IsFavoritesId(favoritesId);
+ }
+
+ ///
+ /// 是否为收藏夹ur2
+ ///
+ ///
+ ///
+ private static bool IsFavoritesUrl2(string input)
+ {
+ string favoritesId = GetId(input, FavoritesUrl2);
+ return IsFavoritesId(favoritesId.Split('/')[0]);
+ }
+
+ ///
+ /// 获取收藏夹id
+ ///
+ ///
+ ///
+ public static long GetFavoritesId(string input)
+ {
+ if (IsFavoritesId(input))
+ {
+ return Number.GetInt(input.Remove(0, 2));
+ }
+ else if (IsFavoritesUrl1(input))
+ {
+ return Number.GetInt(GetId(input, FavoritesUrl1).Remove(0, 2));
+ }
+ else if (IsFavoritesUrl2(input))
+ {
+ return Number.GetInt(GetId(input, FavoritesUrl2).Remove(0, 2).Split('/')[0]);
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ #endregion
+
+ #region 用户空间
+
+ ///
+ /// 是否为用户id
+ ///
+ ///
+ ///
+ public static bool IsUserId(string input)
+ {
+ if (input.ToLower().StartsWith("uid:"))
+ {
+ return Regex.IsMatch(input.Remove(0, 4), @"^\d+$");
+ }
+ else if (input.ToLower().StartsWith("uid"))
+ {
+ return Regex.IsMatch(input.Remove(0, 3), @"^\d+$");
+ }
+ else { return false; }
+ }
+
+ ///
+ /// 是否为用户空间url
+ ///
+ ///
+ ///
+ public static bool IsUserUrl(string input)
+ {
+ if (!IsUrl(input)) { return false; }
+
+ if (input.Contains("space.bilibili.com"))
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 获取用户mid
+ ///
+ ///
+ ///
+ public static long GetUserId(string input)
+ {
+ if (input.ToLower().StartsWith("uid:"))
+ {
+ return Number.GetInt(input.Remove(0, 4));
+ }
+ else if (input.ToLower().StartsWith("uid"))
+ {
+ return Number.GetInt(input.Remove(0, 3));
+ }
+ else if (IsUserUrl(input))
+ {
+ string url = EnableHttps(input);
+ url = DeleteUrlParam(url);
+ var match = Regex.Match(url, @"\d+");
+ if (match.Success)
+ {
+ return long.Parse(match.Value);
+ }
+ else
+ {
+ return -1;
+ }
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ #endregion
+
+ ///
+ /// 是否为网址
+ ///
+ ///
+ ///
+ private static bool IsUrl(string input)
+ {
+ return input.StartsWith("http://") || input.StartsWith("https://");
+ }
+
+ ///
+ /// 将http转为https
+ ///
+ ///
+ private static string EnableHttps(string url)
+ {
+ if (!IsUrl(url)) { return null; }
+
+ return url.Replace("http://", "https://");
+ }
+
+ ///
+ /// 去除url中的参数
+ ///
+ ///
+ ///
+ private static string DeleteUrlParam(string url)
+ {
+ string[] strList = url.Split('?');
+
+ return strList[0].EndsWith("/") ? strList[0].TrimEnd('/') : strList[0];
+ }
+
+ ///
+ /// 从url中获取视频id(avid/bvid)
+ ///
+ ///
+ ///
+ private static string GetVideoId(string input)
+ {
+ return GetId(input, VideoUrl);
+ }
+
+ ///
+ /// 从url中获取番剧id(ss/ep/md)
+ ///
+ ///
+ ///
+ private static string GetBangumiId(string input)
+ {
+ string id = GetId(input, BangumiUrl);
+ if (id != "") { return id; }
+ return GetId(input, BangumiMediaUrl);
+ }
+
+ ///
+ /// 从url中获取课程id(ss/ep)
+ ///
+ ///
+ ///
+ private static string GetCheeseId(string input)
+ {
+ return GetId(input, CheeseUrl);
+ }
+
+ ///
+ /// 是否为数字型id
+ ///
+ ///
+ ///
+ ///
+ private static bool IsIntId(string input, string prefix)
+ {
+ if (input.ToLower().StartsWith(prefix))
+ {
+ return Regex.IsMatch(input.Remove(0, 2), @"^\d+$");
+ }
+ return false;
+ }
+
+ ///
+ /// 从url中获取id
+ ///
+ ///
+ ///
+ ///
+ private static string GetId(string input, string baseUrl)
+ {
+ if (!IsUrl(input)) { return ""; }
+
+ string url = EnableHttps(input);
+ url = DeleteUrlParam(url);
+
+ url = url.Replace(ShareWwwUrl, WwwUrl);
+ url = url.Replace(MobileUrl, WwwUrl);
+
+ if (url.Contains("b23.tv/ss") || url.Contains("b23.tv/ep"))
+ {
+ url = url.Replace(ShortUrl, BangumiUrl);
+ }
+ else
+ {
+ url = url.Replace(ShortUrl, VideoUrl);
+ }
+
+ if (!url.StartsWith(baseUrl)) { return ""; }
+
+ return url.Replace(baseUrl, "");
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Utils/QualityList.cs b/src/Downkyi.Core/Bili/Utils/QualityList.cs
new file mode 100644
index 0000000..b9e30a1
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Utils/QualityList.cs
@@ -0,0 +1,73 @@
+using Downkyi.Core.Bili.Models;
+
+namespace Downkyi.Core.Bili.Utils;
+
+public static class QualityList
+{
+ private static readonly List resolutions = new()
+ {
+ new Quality { Name = "360P 流畅", Id = 16 },
+ new Quality { Name = "480P 清晰", Id = 32 },
+ new Quality { Name = "720P 高清", Id = 64 },
+ new Quality { Name = "720P 60帧", Id = 74 },
+ new Quality { Name = "1080P 高清", Id = 80 },
+ new Quality { Name = "1080P 高码率", Id = 112 },
+ new Quality { Name = "1080P 60帧", Id = 116 },
+ new Quality { Name = "4K 超清", Id = 120 },
+ new Quality { Name = "HDR 真彩", Id = 125 },
+ new Quality { Name = "杜比视界", Id = 126 },
+ new Quality { Name = "超高清 8K", Id = 127 },
+ };
+
+ private static readonly List codecIds = new()
+ {
+ new Quality { Name = "H.264/AVC", Id = 7 },
+ new Quality { Name = "H.265/HEVC", Id = 12 },
+ new Quality { Name = "AV1", Id = 13 },
+ };
+
+ private static readonly List qualities = new()
+ {
+ new Quality { Name = "低质量", Id = 30216 },
+ new Quality { Name = "中质量", Id = 30232 },
+ new Quality { Name = "高质量", Id = 30280 },
+ new Quality { Name = "Dolby Atmos", Id = 30250 },
+ new Quality { Name = "Hi-Res无损", Id = 30251 },
+ };
+
+ ///
+ /// 获取支持的视频画质
+ ///
+ ///
+ public static List GetResolutions()
+ {
+ // 使用深复制,
+ // 保证外部修改list后,
+ // 不会影响其他调用处
+ return new List(resolutions);
+ }
+
+ ///
+ /// 获取视频编码代码
+ ///
+ ///
+ public static List GetCodecIds()
+ {
+ // 使用深复制,
+ // 保证外部修改list后,
+ // 不会影响其他调用处
+ return new List(codecIds);
+ }
+
+ ///
+ /// 获取支持的视频音质
+ ///
+ ///
+ public static List GetAudioQualities()
+ {
+ // 使用深复制,
+ // 保证外部修改list后,
+ // 不会影响其他调用处
+ return new List(qualities);
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Web/Login.cs b/src/Downkyi.Core/Bili/Web/Login.cs
new file mode 100644
index 0000000..c445cff
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Web/Login.cs
@@ -0,0 +1,72 @@
+using BiliSharp;
+using BiliSharp.Api.Login;
+using Downkyi.Core.Bili.Models;
+using Downkyi.Core.Settings;
+
+namespace Downkyi.Core.Bili.Web;
+
+public class Login : ILogin
+{
+ ///
+ /// 申请二维码(web端)
+ ///
+ /// (url, key)
+ public Tuple GetQRCodeUrl()
+ {
+ string userAgent = SettingsManager.GetInstance().GetUserAgent();
+ BiliManager.Instance().SetUserAgent(userAgent);
+
+ var qrcode = LoginQR.GenerateQRCode();
+ if (qrcode == null || qrcode.Data == null) { return null; }
+
+ return Tuple.Create(qrcode.Data.Url, qrcode.Data.QrcodeKey);
+ }
+
+ ///
+ /// 扫码登录(web端)
+ ///
+ ///
+ public QRCodeStatus PollQRCode(string qrcodeKey)
+ {
+ string userAgent = SettingsManager.GetInstance().GetUserAgent();
+ BiliManager.Instance().SetUserAgent(userAgent);
+
+ var qrcode = LoginQR.PollQRCode(qrcodeKey);
+ if (qrcode == null || qrcode.Data == null) { return null; }
+
+ return new QRCodeStatus()
+ {
+ Url = qrcode.Data.Url,
+ Token = qrcode.Data.RefreshToken,
+ Timestamp = qrcode.Data.Timestamp,
+ Code = qrcode.Data.Code,
+ Message = qrcode.Data.Message
+ };
+ }
+
+ ///
+ /// 导航栏用户信息
+ ///
+ ///
+ public NavigationInfo GetNavigationInfo()
+ {
+ string userAgent = SettingsManager.GetInstance().GetUserAgent();
+ BiliManager.Instance().SetUserAgent(userAgent);
+ BiliManager.Instance().SetCookies(LoginHelper.GetLoginInfoCookies());
+
+ var origin = LoginInfo.GetNavigationInfo();
+ if (origin == null || origin.Data == null)
+ {
+ return null;
+ }
+
+ return new NavigationInfo()
+ {
+ Mid = origin.Data.Mid,
+ Name = origin.Data.Uname,
+ Header = origin.Data.Face,
+ VipStatus = origin.Data.Vipstatus,
+ IsLogin = origin.Data.Islogin
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Web/LoginHelper.cs b/src/Downkyi.Core/Bili/Web/LoginHelper.cs
new file mode 100644
index 0000000..c0d8943
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Web/LoginHelper.cs
@@ -0,0 +1,137 @@
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Models;
+using Downkyi.Core.Utils.Encryptor;
+using System.Net;
+
+namespace Downkyi.Core.Bili.Web;
+
+public static class LoginHelper
+{
+ // 本地位置
+ private static readonly string LOCAL_LOGIN_INFO = Storage.StorageManager.GetLogin();
+
+ private static readonly EncryptorFile encryptor = new();
+
+ ///
+ /// 保存登录的cookies到文件
+ ///
+ ///
+ ///
+ public static bool SaveLoginInfoCookies(string url)
+ {
+ CookieContainer cookieContainer = Cookies.ParseCookieByUrl(url);
+
+ return SaveLoginInfoCookies(cookieContainer);
+ }
+
+ ///
+ /// 保存cookies到文件
+ ///
+ ///
+ ///
+ public static bool SaveLoginInfoCookies(CookieContainer cookieContainer)
+ {
+ string tempFile = LOCAL_LOGIN_INFO + "-" + Guid.NewGuid().ToString("N");
+
+ bool isSucceed = Cookies.WriteCookiesToDisk(tempFile, cookieContainer);
+ if (isSucceed)
+ {
+ try
+ {
+ encryptor.EncryptFile(tempFile, LOCAL_LOGIN_INFO);
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+ }
+
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ return isSucceed;
+ }
+
+ ///
+ /// 获得登录的cookies
+ ///
+ ///
+ public static CookieContainer GetLoginInfoCookies()
+ {
+ string tempFile = LOCAL_LOGIN_INFO + "-" + Guid.NewGuid().ToString("N");
+
+ if (File.Exists(LOCAL_LOGIN_INFO))
+ {
+ try
+ {
+ encryptor.DecryptFile(LOCAL_LOGIN_INFO, tempFile);
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ return null;
+ }
+ }
+ else { return null; }
+
+ CookieContainer cookies = Cookies.ReadCookiesFromDisk(tempFile);
+
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ return cookies;
+ }
+
+ ///
+ /// 返回登录信息的cookies的字符串
+ ///
+ ///
+ public static string GetLoginInfoCookiesString()
+ {
+ var cookieContainer = GetLoginInfoCookies();
+ if (cookieContainer == null)
+ {
+ return "";
+ }
+
+ return Cookies.GetCookiesString(cookieContainer);
+ }
+
+ ///
+ /// 注销登录
+ ///
+ ///
+ public static bool Logout()
+ {
+ if (File.Exists(LOCAL_LOGIN_INFO))
+ {
+ try
+ {
+ File.Delete(LOCAL_LOGIN_INFO);
+
+ SettingsManager.GetInstance().SetUserInfo(new UserInfoSettings
+ {
+ Mid = -1,
+ Name = "",
+ IsLogin = false,
+ IsVip = false
+ });
+ return true;
+ }
+ catch (IOException e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+ }
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Bili/Web/User.cs b/src/Downkyi.Core/Bili/Web/User.cs
new file mode 100644
index 0000000..be10494
--- /dev/null
+++ b/src/Downkyi.Core/Bili/Web/User.cs
@@ -0,0 +1,5 @@
+namespace Downkyi.Core.Bili.Web;
+
+public class User : IUser
+{
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Downkyi.Core.csproj b/src/Downkyi.Core/Downkyi.Core.csproj
new file mode 100644
index 0000000..483796d
--- /dev/null
+++ b/src/Downkyi.Core/Downkyi.Core.csproj
@@ -0,0 +1,22 @@
+
+
+ net6.0
+ enable
+
+
+ none
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi.Core/Downloader/MultiThreadDownloader.cs b/src/Downkyi.Core/Downloader/MultiThreadDownloader.cs
new file mode 100644
index 0000000..4eaf589
--- /dev/null
+++ b/src/Downkyi.Core/Downloader/MultiThreadDownloader.cs
@@ -0,0 +1,441 @@
+using System.ComponentModel;
+using System.Net;
+
+namespace Downkyi.Core.Downloader;
+
+///
+/// 文件合并改变事件
+///
+///
+///
+public delegate void FileMergeProgressChangedEventHandler(object sender, int e);
+
+///
+/// 多线程下载器
+///
+public class MultiThreadDownloader
+{
+ #region 属性
+
+ private string _url;
+ private bool _rangeAllowed;
+ private readonly HttpWebRequest _request;
+ private Action _requestConfigure = req => req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36";
+
+ #endregion 属性
+
+ #region 公共属性
+
+ ///
+ /// RangeAllowed
+ ///
+ public bool RangeAllowed
+ {
+ get => _rangeAllowed;
+ set => _rangeAllowed = value;
+ }
+
+ ///
+ /// 临时文件夹
+ ///
+ public string TempFileDirectory { get; set; }
+
+ ///
+ /// url地址
+ ///
+ public string Url
+ {
+ get => _url;
+ set => _url = value;
+ }
+
+ ///
+ /// 第几部分
+ ///
+ public int NumberOfParts { get; set; }
+
+ ///
+ /// 已接收字节数
+ ///
+ public long TotalBytesReceived
+ {
+ get
+ {
+ try
+ {
+ lock (this)
+ {
+ return PartialDownloaderList.Where(t => t != null).Sum(t => t.TotalBytesRead);
+ }
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+ }
+
+ ///
+ /// 总进度
+ ///
+ public float TotalProgress { get; private set; }
+
+ ///
+ /// 文件大小
+ ///
+ public long Size { get; private set; }
+
+ ///
+ /// 下载速度
+ ///
+ public float TotalSpeedInBytes
+ {
+ get
+ {
+ lock (this)
+ {
+ return PartialDownloaderList.Sum(t => t.SpeedInBytes);
+ }
+ }
+ }
+
+ ///
+ /// 下载块
+ ///
+ public List PartialDownloaderList { get; }
+
+ ///
+ /// 文件路径
+ ///
+ public string FilePath { get; set; }
+
+ #endregion 公共属性
+
+ #region 变量
+
+ ///
+ /// 总下载进度更新事件
+ ///
+ public event EventHandler TotalProgressChanged;
+
+ ///
+ /// 文件合并完成事件
+ ///
+ public event EventHandler FileMergedComplete;
+
+ ///
+ /// 文件合并事件
+ ///
+ public event FileMergeProgressChangedEventHandler FileMergeProgressChanged;
+
+ private readonly AsyncOperation _aop;
+
+ #endregion 变量
+
+ #region 下载管理器
+
+ ///
+ /// 多线程下载管理器
+ ///
+ ///
+ ///
+ ///
+ ///
+ public MultiThreadDownloader(string sourceUrl, string tempDir, string savePath, int numOfParts)
+ {
+ _url = sourceUrl;
+ NumberOfParts = numOfParts;
+ TempFileDirectory = tempDir;
+ PartialDownloaderList = new List();
+ _aop = AsyncOperationManager.CreateOperation(null);
+ FilePath = savePath;
+#pragma warning disable SYSLIB0014 // 类型或成员已过时
+ _request = WebRequest.Create(sourceUrl) as HttpWebRequest;
+#pragma warning restore SYSLIB0014 // 类型或成员已过时
+ }
+
+ ///
+ /// 多线程下载管理器
+ ///
+ ///
+ ///
+ ///
+ public MultiThreadDownloader(string sourceUrl, string savePath, int numOfParts) : this(sourceUrl, null, savePath, numOfParts)
+ {
+ TempFileDirectory = Environment.GetEnvironmentVariable("temp");
+ }
+
+ ///
+ /// 多线程下载管理器
+ ///
+ ///
+ ///
+ public MultiThreadDownloader(string sourceUrl, int numOfParts) : this(sourceUrl, null, numOfParts)
+ {
+ }
+
+ #endregion 下载管理器
+
+ #region 事件
+
+ private void temp_DownloadPartCompleted(object sender, EventArgs e)
+ {
+ WaitOrResumeAll(PartialDownloaderList, true);
+
+ if (TotalBytesReceived == Size)
+ {
+ UpdateProgress();
+ MergeParts();
+ return;
+ }
+
+ PartialDownloaderList.Sort((x, y) => (int)(y.RemainingBytes - x.RemainingBytes));
+ var rem = PartialDownloaderList[0].RemainingBytes;
+ if (rem < 50 * 1024)
+ {
+ WaitOrResumeAll(PartialDownloaderList, false);
+ return;
+ }
+
+ var from = PartialDownloaderList[0].CurrentPosition + rem / 2;
+ var to = PartialDownloaderList[0].To;
+ if (from > to)
+ {
+ WaitOrResumeAll(PartialDownloaderList, false);
+ return;
+ }
+
+ PartialDownloaderList[0].To = from - 1;
+ WaitOrResumeAll(PartialDownloaderList, false);
+ var temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, true);
+ temp.DownloadPartCompleted += temp_DownloadPartCompleted;
+ temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
+ lock (this)
+ {
+ PartialDownloaderList.Add(temp);
+ }
+ temp.Start(_requestConfigure);
+ }
+
+ private void temp_DownloadPartProgressChanged(object sender, EventArgs e)
+ {
+ UpdateProgress();
+ }
+
+ private void UpdateProgress()
+ {
+ int pr = (int)(TotalBytesReceived * 1d / Size * 100);
+ if (TotalProgress != pr)
+ {
+ TotalProgress = pr;
+ if (TotalProgressChanged != null)
+ {
+ _aop.Post(state => TotalProgressChanged(this, EventArgs.Empty), null);
+ }
+ }
+ }
+
+ #endregion 事件
+
+ #region 方法
+
+ private void CreateFirstPartitions()
+ {
+ Size = GetContentLength(ref _rangeAllowed, ref _url);
+ int maximumPart = (int)(Size / (25 * 1024));
+ maximumPart = maximumPart == 0 ? 1 : maximumPart;
+ if (!_rangeAllowed)
+ {
+ NumberOfParts = 1;
+ }
+ else if (NumberOfParts > maximumPart)
+ {
+ NumberOfParts = maximumPart;
+ }
+
+ for (int i = 0; i < NumberOfParts; i++)
+ {
+ var temp = CreateNew(i, NumberOfParts, Size);
+ temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
+ temp.DownloadPartCompleted += temp_DownloadPartCompleted;
+ lock (this)
+ {
+ PartialDownloaderList.Add(temp);
+ }
+ temp.Start(_requestConfigure);
+ }
+ }
+
+ private void MergeParts()
+ {
+ var mergeOrderedList = PartialDownloaderList.OrderBy(x => x.From);
+ var dir = new FileInfo(FilePath).DirectoryName;
+ Directory.CreateDirectory(dir);
+
+
+ using (var fs = File.OpenWrite(FilePath))
+ {
+ long totalBytesWrite = 0;
+ int mergeProgress = 0;
+ foreach (var item in mergeOrderedList)
+ {
+ using (var pdi = File.OpenRead(item.FullPath))
+ {
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = pdi.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ fs.Write(buffer, 0, read);
+ totalBytesWrite += read;
+ int temp = (int)(totalBytesWrite * 1d / Size * 100);
+ if (temp != mergeProgress && FileMergeProgressChanged != null)
+ {
+ mergeProgress = temp;
+ _aop.Post(state => FileMergeProgressChanged(this, temp), null);
+ }
+ }
+ }
+
+ try
+ {
+ File.Delete(item.FullPath);
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+ }
+
+ if (FileMergedComplete != null)
+ {
+ _aop.Post(state => FileMergedComplete(state, EventArgs.Empty), this);
+ }
+ }
+
+ private PartialDownloader CreateNew(int order, int parts, long contentLength)
+ {
+ var division = contentLength / parts;
+ var remaining = contentLength % parts;
+ var start = division * order;
+ var end = start + division - 1;
+ end += order == parts - 1 ? remaining : 0;
+ return new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString("N"), start, end, true);
+ }
+
+ ///
+ /// 暂停或继续
+ ///
+ ///
+ ///
+ public static void WaitOrResumeAll(List list, bool wait)
+ {
+ for (var index = 0; index < list.Count; index++)
+ {
+ if (wait)
+ {
+ list[index].Wait();
+ }
+ else
+ {
+ list[index].ResumeAfterWait();
+ }
+ }
+ }
+
+ ///
+ /// 配置请求头
+ ///
+ ///
+ public void Configure(Action config)
+ {
+ _requestConfigure = config;
+ }
+
+ ///
+ /// 获取内容长度
+ ///
+ ///
+ ///
+ ///
+ public long GetContentLength(ref bool rangeAllowed, ref string redirectedUrl)
+ {
+ _request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36";
+ _request.ServicePoint.ConnectionLimit = 4;
+ _requestConfigure(_request);
+
+ using (var resp = _request.GetResponse() as HttpWebResponse)
+ {
+ redirectedUrl = resp.ResponseUri.OriginalString;
+ var ctl = resp.ContentLength;
+ rangeAllowed = resp.Headers.AllKeys.Select((v, i) => new
+ {
+ HeaderName = v,
+ HeaderValue = resp.Headers[i]
+ }).Any(k => k.HeaderName.ToLower().Contains("range") && k.HeaderValue.ToLower().Contains("byte"));
+ _request.Abort();
+ return ctl;
+ }
+ }
+
+ #endregion 方法
+
+ #region 公共方法
+
+ ///
+ /// 暂停下载
+ ///
+ public void Pause()
+ {
+ lock (this)
+ {
+ foreach (var t in PartialDownloaderList.Where(t => !t.Completed))
+ {
+ t.Stop();
+ }
+ }
+
+ Thread.Sleep(200);
+ }
+
+ ///
+ /// 开始下载
+ ///
+ public void Start()
+ {
+ Task th = new Task(CreateFirstPartitions);
+ th.Start();
+ }
+
+ ///
+ /// 唤醒下载
+ ///
+ public void Resume()
+ {
+ int count = PartialDownloaderList.Count;
+ for (int i = 0; i < count; i++)
+ {
+ if (PartialDownloaderList[i].Stopped)
+ {
+ var from = PartialDownloaderList[i].CurrentPosition + 1;
+ var to = PartialDownloaderList[i].To;
+ if (from > to)
+ {
+ continue;
+ }
+
+ var temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, _rangeAllowed);
+ temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
+ temp.DownloadPartCompleted += temp_DownloadPartCompleted;
+ lock (this)
+ {
+ PartialDownloaderList.Add(temp);
+ }
+ PartialDownloaderList[i].To = PartialDownloaderList[i].CurrentPosition;
+ temp.Start(_requestConfigure);
+ }
+ }
+ }
+
+ #endregion 公共方法
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Downloader/PartialDownloader.cs b/src/Downkyi.Core/Downloader/PartialDownloader.cs
new file mode 100644
index 0000000..a832f6a
--- /dev/null
+++ b/src/Downkyi.Core/Downloader/PartialDownloader.cs
@@ -0,0 +1,262 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Net;
+
+namespace Downkyi.Core.Downloader;
+
+///
+/// 部分下载器
+///
+public class PartialDownloader
+{
+ ///
+ /// 这部分完成事件
+ ///
+ public event EventHandler DownloadPartCompleted;
+
+ ///
+ /// 部分下载进度改变事件
+ ///
+ public event EventHandler DownloadPartProgressChanged;
+
+ ///
+ /// 部分下载停止事件
+ ///
+ public event EventHandler DownloadPartStopped;
+
+ private readonly AsyncOperation _aop = AsyncOperationManager.CreateOperation(null);
+ private readonly int[] _lastSpeeds;
+ private long _counter;
+ private long _to;
+ private long _totalBytesRead;
+ private bool _wait;
+
+ ///
+ /// 下载已停止
+ ///
+ public bool Stopped { get; private set; }
+
+ ///
+ /// 下载已完成
+ ///
+ public bool Completed { get; private set; }
+
+ ///
+ /// 下载进度
+ ///
+ public int Progress { get; private set; }
+
+ ///
+ /// 下载目录
+ ///
+ public string Directory { get; }
+
+ ///
+ /// 文件名
+ ///
+ public string FileName { get; }
+
+ ///
+ /// 已读字节数
+ ///
+ public long TotalBytesRead => _totalBytesRead;
+
+ ///
+ /// 内容长度
+ ///
+ public long ContentLength { get; private set; }
+
+ ///
+ /// RangeAllowed
+ ///
+ public bool RangeAllowed { get; }
+
+ ///
+ /// url
+ ///
+ public string Url { get; }
+
+ ///
+ /// to
+ ///
+ public long To
+ {
+ get => _to;
+ set
+ {
+ _to = value;
+ ContentLength = _to - From + 1;
+ }
+ }
+
+ ///
+ /// from
+ ///
+ public long From { get; }
+
+ ///
+ /// 当前位置
+ ///
+ public long CurrentPosition => From + _totalBytesRead - 1;
+
+ ///
+ /// 剩余字节数
+ ///
+ public long RemainingBytes => ContentLength - _totalBytesRead;
+
+ ///
+ /// 完整路径
+ ///
+ public string FullPath => Path.Combine(Directory, FileName);
+
+ ///
+ /// 下载速度
+ ///
+ public int SpeedInBytes
+ {
+ get
+ {
+ if (Completed)
+ {
+ return 0;
+ }
+
+ int totalSpeeds = _lastSpeeds.Sum();
+ return totalSpeeds / 10;
+ }
+ }
+
+ ///
+ /// 部分块下载
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public PartialDownloader(string url, string dir, string fileGuid, long from, long to, bool rangeAllowed)
+ {
+ From = from;
+ _to = to;
+ Url = url;
+ RangeAllowed = rangeAllowed;
+ FileName = fileGuid;
+ Directory = dir;
+ _lastSpeeds = new int[10];
+ }
+
+ private void DownloadProcedure(Action config)
+ {
+ using var file = new FileStream(FullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete);
+ var sw = new Stopwatch();
+#pragma warning disable SYSLIB0014 // 类型或成员已过时
+ if (WebRequest.Create(Url) is HttpWebRequest req)
+ {
+ req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36";
+ req.AllowAutoRedirect = true;
+ req.MaximumAutomaticRedirections = 5;
+ req.ServicePoint.ConnectionLimit += 1;
+ req.ServicePoint.Expect100Continue = true;
+ req.ProtocolVersion = HttpVersion.Version11;
+ req.Proxy = WebRequest.GetSystemWebProxy();
+ config(req);
+ if (RangeAllowed)
+ {
+ req.AddRange(From, _to);
+ }
+
+ if (req.GetResponse() is HttpWebResponse resp)
+ {
+ ContentLength = resp.ContentLength;
+ if (ContentLength <= 0 || (RangeAllowed && ContentLength != _to - From + 1))
+ {
+ throw new Exception("Invalid response content");
+ }
+
+ using var tempStream = resp.GetResponseStream();
+ int bytesRead;
+ byte[] buffer = new byte[4096];
+ sw.Start();
+ while ((bytesRead = tempStream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ if (_totalBytesRead + bytesRead > ContentLength)
+ {
+ bytesRead = (int)(ContentLength - _totalBytesRead);
+ }
+
+ file.Write(buffer, 0, bytesRead);
+ _totalBytesRead += bytesRead;
+ _lastSpeeds[_counter] = (int)(_totalBytesRead / Math.Ceiling(sw.Elapsed.TotalSeconds));
+ _counter = (_counter >= 9) ? 0 : _counter + 1;
+ int tempProgress = (int)(_totalBytesRead * 100 / ContentLength);
+ if (Progress != tempProgress)
+ {
+ Progress = tempProgress;
+ _aop.Post(state =>
+ {
+ DownloadPartProgressChanged?.Invoke(this, EventArgs.Empty);
+ }, null);
+ }
+
+ if (Stopped || (RangeAllowed && _totalBytesRead == ContentLength))
+ {
+ break;
+ }
+ }
+ }
+
+ req.Abort();
+ }
+#pragma warning restore SYSLIB0014 // 类型或成员已过时
+
+ sw.Stop();
+ if (!Stopped && DownloadPartCompleted != null)
+ {
+ _aop.Post(state =>
+ {
+ Completed = true;
+ DownloadPartCompleted(this, EventArgs.Empty);
+ }, null);
+ }
+
+ if (Stopped && DownloadPartStopped != null)
+ {
+ _aop.Post(state => DownloadPartStopped(this, EventArgs.Empty), null);
+ }
+ }
+
+ ///
+ /// 启动下载
+ ///
+ public void Start(Action config)
+ {
+ Stopped = false;
+ var procThread = new Thread(_ => DownloadProcedure(config));
+ procThread.Start();
+ }
+
+ ///
+ /// 下载停止
+ ///
+ public void Stop()
+ {
+ Stopped = true;
+ }
+
+ ///
+ /// 暂停等待下载
+ ///
+ public void Wait()
+ {
+ _wait = true;
+ }
+
+ ///
+ /// 稍后唤醒
+ ///
+ public void ResumeAfterWait()
+ {
+ _wait = false;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/FFmpeg/FFmpegHelper.cs b/src/Downkyi.Core/FFmpeg/FFmpegHelper.cs
new file mode 100644
index 0000000..ac6a878
--- /dev/null
+++ b/src/Downkyi.Core/FFmpeg/FFmpegHelper.cs
@@ -0,0 +1,243 @@
+using System.Diagnostics;
+
+namespace Downkyi.Core.FFmpeg;
+
+public static class FFmpegHelper
+{
+ private const string Tag = "FFmpegHelper";
+
+ private static readonly bool is64Bit = false;
+ private static readonly string exec = "";
+
+ static FFmpegHelper()
+ {
+ is64Bit = IntPtr.Size == 8;
+
+ if (is64Bit)
+ {
+ exec = Path.Combine(Environment.CurrentDirectory, "ffmpeg.exe");
+ }
+ else
+ {
+ exec = Path.Combine(Environment.CurrentDirectory, "ffmpeg.exe");
+ }
+ }
+
+ ///
+ /// 合并音频和视频
+ ///
+ /// 音频
+ /// 视频
+ ///
+ public static bool MergeVideo(string video1, string video2, string destVideo)
+ {
+ string param = $"-y -i \"{video1}\" -i \"{video2}\" -strict -2 -acodec copy -vcodec copy -f mp4 \"{destVideo}\"";
+ if (video1 == null || !File.Exists(video1))
+ {
+ param = $"-y -i \"{video2}\" -strict -2 -acodec copy -vcodec copy -f mp4 \"{destVideo}\"";
+ }
+ if (video2 == null || !File.Exists(video2))
+ {
+ param = $"-y -i \"{video1}\" -strict -2 -acodec copy \"{destVideo}\"";
+ }
+
+ // 支持flac格式音频
+ //param += " -strict -2";
+
+ if (!File.Exists(video1) && !File.Exists(video2)) { return false; }
+
+ // 如果存在
+ try { File.Delete(destVideo); }
+ catch (IOException e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+
+ ExcuteProcess(exec, param, null, (s, e) => Console.WriteLine(e.Data));
+
+ try
+ {
+ if (video1 != null) { File.Delete(video1); }
+ if (video2 != null) { File.Delete(video2); }
+ }
+ catch (IOException e)
+ {
+ Log.Log.Logger.Error(e);
+ }
+
+ return true;
+ }
+
+ ///
+ /// 拼接多个视频
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static bool ConcatVideo(string workingDirectory, List flvFiles, string destVideo)
+ {
+ // contact的文件名,不包含路径
+ string concatFileName = Guid.NewGuid().ToString("N") + "_concat.txt";
+ try
+ {
+ string contact = "";
+ foreach (string flv in flvFiles)
+ {
+ contact += $"file '{flv}'\n";
+ }
+
+ FileStream fileStream = new(workingDirectory + "/" + concatFileName, FileMode.Create);
+ StreamWriter streamWriter = new(fileStream);
+ //开始写入
+ streamWriter.Write(contact);
+ //清空缓冲区
+ streamWriter.Flush();
+ //关闭流
+ streamWriter.Close();
+ fileStream.Close();
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+
+ // ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy output.mkv
+ // 加上-y,表示如果有同名文件,则默认覆盖
+ string param = $"-y -f concat -safe 0 -i {concatFileName} -c copy \"{destVideo}\" -y";
+ ExcuteProcess(exec, param, workingDirectory, (s, e) => Console.WriteLine(e.Data));
+
+ // 删除临时文件
+ try
+ {
+ // 删除concat文件
+ File.Delete(workingDirectory + "/" + concatFileName);
+
+ foreach (string flv in flvFiles)
+ {
+ File.Delete(flv);
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ }
+
+ return true;
+ }
+
+ ///
+ /// 去水印,非常消耗cpu资源
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Delogo(string video, string destVideo, int x, int y, int width, int height, Action action)
+ {
+ // ffmpeg -y -i "video.mp4" -vf delogo=x=1670:y=50:w=180:h=70:show=1 "delogo.mp4"
+ string param = $"-y -i \"{video}\" -vf delogo=x={x}:y={y}:w={width}:h={height}:show=0 \"{destVideo}\" -hide_banner";
+ ExcuteProcess(exec, param, null, (s, e) =>
+ {
+ Console.WriteLine(e.Data);
+ action.Invoke(e.Data!);
+ });
+ }
+
+ ///
+ /// 从一个视频中仅提取音频
+ ///
+ /// 源视频
+ /// 目标音频
+ /// 输出信息
+ public static void ExtractAudio(string video, string audio, Action action)
+ {
+ // 抽取音频命令
+ // ffmpeg -i 3.mp4 -vn -y -acodec copy 3.aac
+ // ffmpeg -i 3.mp4 -vn -y -acodec copy 3.m4a
+ string param = $"-i \"{video}\" -vn -y -acodec copy \"{audio}\" -hide_banner";
+ ExcuteProcess(exec, param,
+ null, (s, e) =>
+ {
+ Console.WriteLine(e.Data);
+ action.Invoke(e.Data!);
+ });
+ }
+
+ ///
+ /// 从一个视频中仅提取视频
+ ///
+ /// 源视频
+ /// 目标视频
+ /// 输出信息
+ public static void ExtractVideo(string video, string destVideo, Action action)
+ {
+ // 提取视频 (Extract Video)
+ // ffmpeg -i Life.of.Pi.has.subtitles.mkv -vcodec copy –an videoNoAudioSubtitle.mp4
+ string param = $"-i \"{video}\" -y -vcodec copy -an \"{destVideo}\" -hide_banner";
+ ExcuteProcess(exec, param,
+ null, (s, e) =>
+ {
+ Console.WriteLine(e.Data);
+ action.Invoke(e.Data!);
+ });
+ }
+
+ ///
+ /// 提取视频的帧,输出为图片
+ ///
+ ///
+ ///
+ ///
+ public static void ExtractFrame(string video, string image, uint number)
+ {
+ // 提取帧
+ // ffmpeg -i caiyilin.wmv -vframes 1 wm.bmp
+ string param = $"-i \"{video}\" -y -vframes {number} \"{image}\"";
+ ExcuteProcess(exec, param, null, (s, e) => Console.WriteLine(e.Data));
+ }
+
+
+ ///
+ /// 执行一个控制台程序
+ ///
+ /// 程序名称
+ /// 参数
+ /// 工作路径
+ /// 输出重定向
+ private static void ExcuteProcess(string exe, string arg, string workingDirectory, DataReceivedEventHandler output)
+ {
+ using var p = new Process();
+ p.StartInfo.FileName = exe;
+ p.StartInfo.Arguments = arg;
+
+ // 工作目录
+ if (workingDirectory != null)
+ {
+ p.StartInfo.WorkingDirectory = workingDirectory;
+ }
+
+ p.StartInfo.UseShellExecute = false; //输出信息重定向
+ p.StartInfo.CreateNoWindow = true;
+ p.StartInfo.RedirectStandardError = true;
+ p.StartInfo.RedirectStandardOutput = true;
+
+ // 将 StandardErrorEncoding 改为 UTF-8 才不会出现中文乱码
+ p.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
+ p.StartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8;
+
+ p.OutputDataReceived += output;
+ p.ErrorDataReceived += output;
+
+ p.Start(); //启动线程
+ p.BeginOutputReadLine();
+ p.BeginErrorReadLine();
+ p.WaitForExit(); //等待进程结束
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/FileName/FileName.cs b/src/Downkyi.Core/FileName/FileName.cs
new file mode 100644
index 0000000..310b1a3
--- /dev/null
+++ b/src/Downkyi.Core/FileName/FileName.cs
@@ -0,0 +1,192 @@
+using System.Text.RegularExpressions;
+
+namespace Downkyi.Core.FileName;
+
+public class FileName
+{
+ private readonly List nameParts;
+ private string order = "ORDER";
+ private string section = "SECTION";
+ private string mainTitle = "MAIN_TITLE";
+ private string pageTitle = "PAGE_TITLE";
+ private string videoZone = "VIDEO_ZONE";
+ private string audioQuality = "AUDIO_QUALITY";
+ private string videoQuality = "VIDEO_QUALITY";
+ private string videoCodec = "VIDEO_CODEC";
+
+ private string videoPublishTime = "VIDEO_PUBLISH_TIME";
+
+ private long avid = -1;
+ private string bvid = "BVID";
+ private long cid = -1;
+
+ private long upMid = -1;
+ private string upName = "UP_NAME";
+
+ private FileName(List nameParts)
+ {
+ this.nameParts = nameParts;
+ }
+
+ public static FileName Builder(List nameParts)
+ {
+ return new FileName(nameParts);
+ }
+
+ public FileName SetOrder(int order)
+ {
+ this.order = order.ToString();
+ return this;
+ }
+
+ public FileName SetOrder(int order, int count)
+ {
+ int length = Math.Abs(count).ToString().Length;
+ this.order = order.ToString("D" + length);
+
+ return this;
+ }
+
+ public FileName SetSection(string section)
+ {
+ this.section = section;
+ return this;
+ }
+
+ public FileName SetMainTitle(string mainTitle)
+ {
+ this.mainTitle = mainTitle;
+ return this;
+ }
+
+ public FileName SetPageTitle(string pageTitle)
+ {
+ this.pageTitle = pageTitle;
+ return this;
+ }
+
+ public FileName SetVideoZone(string videoZone)
+ {
+ this.videoZone = videoZone;
+ return this;
+ }
+
+ public FileName SetAudioQuality(string audioQuality)
+ {
+ this.audioQuality = audioQuality;
+ return this;
+ }
+
+ public FileName SetVideoQuality(string videoQuality)
+ {
+ this.videoQuality = videoQuality;
+ return this;
+ }
+
+ public FileName SetVideoCodec(string videoCodec)
+ {
+ this.videoCodec = videoCodec;
+ return this;
+ }
+
+ public FileName SetVideoPublishTime(string videoPublishTime)
+ {
+ this.videoPublishTime = videoPublishTime;
+ return this;
+ }
+
+ public FileName SetAvid(long avid)
+ {
+ this.avid = avid;
+ return this;
+ }
+
+ public FileName SetBvid(string bvid)
+ {
+ this.bvid = bvid;
+ return this;
+ }
+
+ public FileName SetCid(long cid)
+ {
+ this.cid = cid;
+ return this;
+ }
+
+ public FileName SetUpMid(long upMid)
+ {
+ this.upMid = upMid;
+ return this;
+ }
+
+ public FileName SetUpName(string upName)
+ {
+ this.upName = upName;
+ return this;
+ }
+
+ public string RelativePath()
+ {
+ string path = string.Empty;
+
+ foreach (FileNamePart part in nameParts)
+ {
+ switch (part)
+ {
+ case FileNamePart.ORDER:
+ path += order;
+ break;
+ case FileNamePart.SECTION:
+ path += section;
+ break;
+ case FileNamePart.MAIN_TITLE:
+ path += mainTitle;
+ break;
+ case FileNamePart.PAGE_TITLE:
+ path += pageTitle;
+ break;
+ case FileNamePart.VIDEO_ZONE:
+ path += videoZone;
+ break;
+ case FileNamePart.AUDIO_QUALITY:
+ path += audioQuality;
+ break;
+ case FileNamePart.VIDEO_QUALITY:
+ path += videoQuality;
+ break;
+ case FileNamePart.VIDEO_CODEC:
+ path += videoCodec;
+ break;
+ case FileNamePart.VIDEO_PUBLISH_TIME:
+ path += videoPublishTime;
+ break;
+ case FileNamePart.AVID:
+ path += avid;
+ break;
+ case FileNamePart.BVID:
+ path += bvid;
+ break;
+ case FileNamePart.CID:
+ path += cid;
+ break;
+ case FileNamePart.UP_MID:
+ path += upMid;
+ break;
+ case FileNamePart.UP_NAME:
+ path += upName;
+ break;
+ }
+
+ if (((int)part) >= 100)
+ {
+ path += HyphenSeparated.Hyphen[(int)part];
+ }
+ }
+
+ // 避免连续多个斜杠
+ path = Regex.Replace(path, @"//+", "/");
+ // 避免以斜杠开头和结尾的情况
+ return path.TrimEnd('/').TrimStart('/');
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/FileName/FileNamePart.cs b/src/Downkyi.Core/FileName/FileNamePart.cs
new file mode 100644
index 0000000..50ed2eb
--- /dev/null
+++ b/src/Downkyi.Core/FileName/FileNamePart.cs
@@ -0,0 +1,43 @@
+namespace Downkyi.Core.FileName;
+
+public enum FileNamePart
+{
+ // Video
+ ORDER = 1,
+ SECTION,
+ MAIN_TITLE,
+ PAGE_TITLE,
+ VIDEO_ZONE,
+ AUDIO_QUALITY,
+ VIDEO_QUALITY,
+ VIDEO_CODEC,
+
+ VIDEO_PUBLISH_TIME,
+
+ AVID,
+ BVID,
+ CID,
+
+ UP_MID,
+ UP_NAME,
+
+ // 斜杠
+ SLASH = 100,
+
+ // HyphenSeparated
+ UNDERSCORE = 101, // 下划线
+ HYPHEN, // 连字符
+ PLUS, // 加号
+ COMMA, // 逗号
+ PERIOD, // 句号
+ AND, // and
+ NUMBER, // #
+ OPEN_PAREN, // 左圆括号
+ CLOSE_PAREN, // 右圆括号
+ OPEN_BRACKET, // 左方括号
+ CLOSE_BRACKET, // 右方括号
+ OPEN_BRACE, // 左花括号
+ CLOSE_brace, // 右花括号
+ BLANK, // 空白符
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/FileName/HyphenSeparated.cs b/src/Downkyi.Core/FileName/HyphenSeparated.cs
new file mode 100644
index 0000000..1d6bbb7
--- /dev/null
+++ b/src/Downkyi.Core/FileName/HyphenSeparated.cs
@@ -0,0 +1,28 @@
+namespace Downkyi.Core.FileName;
+
+///
+/// 文件名字段
+///
+public static class HyphenSeparated
+{
+ // 文件名的分隔符
+ public static Dictionary Hyphen = new Dictionary()
+ {
+ { 100, "/" },
+ { 101, "_" },
+ { 102, "-" },
+ { 103, "+" },
+ { 104, "," },
+ { 105, "." },
+ { 106, "&" },
+ { 107, "#" },
+ { 108, "(" },
+ { 109, ")" },
+ { 110, "[" },
+ { 111, "]" },
+ { 112, "{" },
+ { 113, "}" },
+ { 114, " " },
+ };
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Logger/Log.cs b/src/Downkyi.Core/Logger/Log.cs
new file mode 100644
index 0000000..8595d9f
--- /dev/null
+++ b/src/Downkyi.Core/Logger/Log.cs
@@ -0,0 +1,47 @@
+using NLog;
+
+namespace Downkyi.Core.Log;
+
+public static class Log
+{
+ static Log()
+ {
+ var config = new NLog.Config.LoggingConfiguration();
+
+ // Targets where to log to Debugger
+ var logConsole = new NLog.Targets.ConsoleTarget("logConsole")
+ {
+ Layout = "${date:format=HH\\:mm\\:ss} [${level:padding=-5}] ${message}"
+ };
+
+ // Targets where to log to Debugger
+ var logDebugger = new NLog.Targets.DebuggerTarget("logDebugger")
+ {
+ Layout = "${date:format=HH\\:mm\\:ss} [${level:padding=-5}] ${message}"
+ };
+
+ // Targets where to log to File
+ var logFile = new NLog.Targets.FileTarget("logFile")
+ {
+ FileName = "${baseDir}/Logs/${shortdate}/${level}.log",
+ CreateDirs = true,
+ KeepFileOpen = true,
+ ArchiveAboveSize = 1024 * 1024,
+ ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Sequence,
+ MaxArchiveDays = 30,
+ Layout = "${longdate} [${level:uppercase=false:padding=-5}] ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}"
+ };
+
+ // Rules for mapping loggers to targets
+ config.AddRule(LogLevel.Trace, LogLevel.Fatal, logConsole);
+ config.AddRule(LogLevel.Trace, LogLevel.Fatal, logDebugger);
+ config.AddRule(LogLevel.Debug, LogLevel.Fatal, logFile);
+
+ // Apply config
+ LogManager.Configuration = config;
+
+ LogManager.ThrowExceptions = false;
+ }
+
+ public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+}
diff --git a/src/Downkyi.Core/Settings/Enum/AfterDownloadOperation.cs b/src/Downkyi.Core/Settings/Enum/AfterDownloadOperation.cs
new file mode 100644
index 0000000..745e3e5
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/AfterDownloadOperation.cs
@@ -0,0 +1,10 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum AfterDownloadOperation
+{
+ NOT_SET = 0,
+ NONE = 1,
+ OPEN_FOLDER,
+ CLOSE_APP,
+ CLOSE_SYSTEM
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/AllowStatus.cs b/src/Downkyi.Core/Settings/Enum/AllowStatus.cs
new file mode 100644
index 0000000..d901f63
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/AllowStatus.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum AllowStatus
+{
+ NONE = 0,
+ NO,
+ YES
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/DanmakuLayoutAlgorithm.cs b/src/Downkyi.Core/Settings/Enum/DanmakuLayoutAlgorithm.cs
new file mode 100644
index 0000000..749f7e9
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/DanmakuLayoutAlgorithm.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum DanmakuLayoutAlgorithm
+{
+ NONE = 0,
+ ASYNC,
+ SYNC
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/DownloadFinishedSort.cs b/src/Downkyi.Core/Settings/Enum/DownloadFinishedSort.cs
new file mode 100644
index 0000000..611da79
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/DownloadFinishedSort.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum DownloadFinishedSort
+{
+ NOT_SET = 0,
+ DOWNLOAD,
+ NUMBER
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/Downloader.cs b/src/Downkyi.Core/Settings/Enum/Downloader.cs
new file mode 100644
index 0000000..7bed2a6
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/Downloader.cs
@@ -0,0 +1,9 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum Downloader
+{
+ NOT_SET = 0,
+ BUILT_IN,
+ ARIA,
+ CUSTOM_ARIA,
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/OrderFormat.cs b/src/Downkyi.Core/Settings/Enum/OrderFormat.cs
new file mode 100644
index 0000000..d56d260
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/OrderFormat.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum OrderFormat
+{
+ NOT_SET = 0,
+ NATURAL, // 自然数
+ LEADING_ZEROS, // 前导零填充
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/ParseScope.cs b/src/Downkyi.Core/Settings/Enum/ParseScope.cs
new file mode 100644
index 0000000..ac9485d
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/ParseScope.cs
@@ -0,0 +1,10 @@
+namespace Downkyi.Core.Settings.Enum;
+
+public enum ParseScope
+{
+ NOT_SET = 0,
+ NONE = 1,
+ SELECTED_ITEM,
+ CURRENT_SECTION,
+ ALL
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Enum/VideoCodecs.cs b/src/Downkyi.Core/Settings/Enum/VideoCodecs.cs
new file mode 100644
index 0000000..3fd07ff
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Enum/VideoCodecs.cs
@@ -0,0 +1,9 @@
+namespace Downkyi.Core.Settings.Enum
+{
+ public enum VideoCodecs
+ {
+ NONE = 0,
+ AVC,
+ HEVC
+ }
+}
diff --git a/src/Downkyi.Core/Settings/Models/AboutSettings.cs b/src/Downkyi.Core/Settings/Models/AboutSettings.cs
new file mode 100644
index 0000000..d930935
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/AboutSettings.cs
@@ -0,0 +1,12 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings.Models;
+
+///
+/// 关于
+///
+public class AboutSettings
+{
+ public AllowStatus IsReceiveBetaVersion { get; set; } = AllowStatus.NONE;
+ public AllowStatus AutoUpdateWhenLaunch { get; set; } = AllowStatus.NONE;
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/AppSettings.cs b/src/Downkyi.Core/Settings/Models/AppSettings.cs
new file mode 100644
index 0000000..9ef0182
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/AppSettings.cs
@@ -0,0 +1,11 @@
+namespace Downkyi.Core.Settings.Models;
+
+public class AppSettings
+{
+ public BasicSettings Basic { get; set; } = new BasicSettings();
+ public NetworkSettings Network { get; set; } = new NetworkSettings();
+ public VideoSettings Video { get; set; } = new VideoSettings();
+ public DanmakuSettings Danmaku { get; set; } = new DanmakuSettings();
+ public AboutSettings About { get; set; } = new AboutSettings();
+ public UserInfoSettings UserInfo { get; set; } = new UserInfoSettings();
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/BasicSettings.cs b/src/Downkyi.Core/Settings/Models/BasicSettings.cs
new file mode 100644
index 0000000..54ac206
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/BasicSettings.cs
@@ -0,0 +1,16 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings.Models;
+
+///
+/// 基本
+///
+public class BasicSettings
+{
+ public AfterDownloadOperation AfterDownload { get; set; } = AfterDownloadOperation.NOT_SET;
+ public AllowStatus IsListenClipboard { get; set; } = AllowStatus.NONE;
+ public AllowStatus IsAutoParseVideo { get; set; } = AllowStatus.NONE;
+ public ParseScope ParseScope { get; set; } = ParseScope.NOT_SET;
+ public AllowStatus IsAutoDownloadAll { get; set; } = AllowStatus.NONE;
+ public DownloadFinishedSort DownloadFinishedSort { get; set; } = DownloadFinishedSort.NOT_SET;
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/DanmakuSettings.cs b/src/Downkyi.Core/Settings/Models/DanmakuSettings.cs
new file mode 100644
index 0000000..841aa6c
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/DanmakuSettings.cs
@@ -0,0 +1,20 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings.Models;
+
+///
+/// 弹幕
+///
+public class DanmakuSettings
+{
+ public AllowStatus DanmakuTopFilter { get; set; } = AllowStatus.NONE;
+ public AllowStatus DanmakuBottomFilter { get; set; } = AllowStatus.NONE;
+ public AllowStatus DanmakuScrollFilter { get; set; } = AllowStatus.NONE;
+ public AllowStatus IsCustomDanmakuResolution { get; set; } = AllowStatus.NONE;
+ public int DanmakuScreenWidth { get; set; } = -1;
+ public int DanmakuScreenHeight { get; set; } = -1;
+ public string DanmakuFontName { get; set; } = null;
+ public int DanmakuFontSize { get; set; } = -1;
+ public int DanmakuLineCount { get; set; } = -1;
+ public DanmakuLayoutAlgorithm DanmakuLayoutAlgorithm { get; set; } = DanmakuLayoutAlgorithm.NONE;
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/NetworkSettings.cs b/src/Downkyi.Core/Settings/Models/NetworkSettings.cs
new file mode 100644
index 0000000..239926a
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/NetworkSettings.cs
@@ -0,0 +1,40 @@
+using Aria2cNet.Server;
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings.Models;
+
+///
+/// 网络
+///
+public class NetworkSettings
+{
+ public AllowStatus IsLiftingOfRegion { get; set; } = AllowStatus.NONE;
+
+ public AllowStatus UseSSL { get; set; } = AllowStatus.NONE;
+ public string UserAgent { get; set; } = string.Empty;
+
+ public Enum.Downloader Downloader { get; set; } = Enum.Downloader.NOT_SET;
+ public int MaxCurrentDownloads { get; set; } = -1;
+
+ #region built-in
+ public int Split { get; set; } = -1;
+ public AllowStatus IsHttpProxy { get; set; } = AllowStatus.NONE;
+ public string HttpProxy { get; set; } = null;
+ public int HttpProxyListenPort { get; set; } = -1;
+ #endregion
+
+ #region Aria
+ public string AriaToken { get; set; } = null;
+ public string AriaHost { get; set; } = null;
+ public int AriaListenPort { get; set; } = -1;
+ public AriaConfigLogLevel AriaLogLevel { get; set; } = AriaConfigLogLevel.NOT_SET;
+ public int AriaSplit { get; set; } = -1;
+ public int AriaMaxOverallDownloadLimit { get; set; } = -1;
+ public int AriaMaxDownloadLimit { get; set; } = -1;
+ public AriaConfigFileAllocation AriaFileAllocation { get; set; } = AriaConfigFileAllocation.NOT_SET;
+
+ public AllowStatus IsAriaHttpProxy { get; set; } = AllowStatus.NONE;
+ public string AriaHttpProxy { get; set; } = null;
+ public int AriaHttpProxyListenPort { get; set; } = -1;
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/UserInfoSettings.cs b/src/Downkyi.Core/Settings/Models/UserInfoSettings.cs
new file mode 100644
index 0000000..c00bfce
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/UserInfoSettings.cs
@@ -0,0 +1,9 @@
+namespace Downkyi.Core.Settings.Models;
+
+public class UserInfoSettings
+{
+ public long Mid { get; set; }
+ public string Name { get; set; }
+ public bool IsLogin { get; set; } // 是否登录
+ public bool IsVip { get; set; } // 是否为大会员,未登录时为false
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/VideoContentSettings.cs b/src/Downkyi.Core/Settings/Models/VideoContentSettings.cs
new file mode 100644
index 0000000..71c0484
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/VideoContentSettings.cs
@@ -0,0 +1,10 @@
+namespace Downkyi.Core.Settings.Models;
+
+public class VideoContentSettings
+{
+ public bool DownloadAudio { get; set; } = true;
+ public bool DownloadVideo { get; set; } = true;
+ public bool DownloadDanmaku { get; set; } = true;
+ public bool DownloadSubtitle { get; set; } = true;
+ public bool DownloadCover { get; set; } = true;
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/Models/VideoSettings.cs b/src/Downkyi.Core/Settings/Models/VideoSettings.cs
new file mode 100644
index 0000000..984981d
--- /dev/null
+++ b/src/Downkyi.Core/Settings/Models/VideoSettings.cs
@@ -0,0 +1,22 @@
+using Downkyi.Core.FileName;
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings.Models;
+
+///
+/// 视频
+///
+public class VideoSettings
+{
+ public int VideoCodecs { get; set; } = -1; // AVC or HEVC
+ public int Quality { get; set; } = -1; // 画质
+ public int AudioQuality { get; set; } = -1; // 音质
+ public AllowStatus IsTranscodingFlvToMp4 { get; set; } = AllowStatus.NONE; // 是否将flv转为mp4
+ public string SaveVideoRootPath { get; set; } = null; // 视频保存路径
+ public List HistoryVideoRootPaths { get; set; } = null; // 历史视频保存路径
+ public AllowStatus IsUseSaveVideoRootPath { get; set; } = AllowStatus.NONE; // 是否使用默认视频保存路径
+ public VideoContentSettings VideoContent { get; set; } = null; // 下载内容
+ public List FileNameParts { get; set; } = null; // 文件命名格式
+ public string FileNamePartTimeFormat { get; set; } = null; // 文件命名中的时间格式
+ public OrderFormat OrderFormat { get; set; } = OrderFormat.NOT_SET; // 文件命名中的序号格式
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.About.cs b/src/Downkyi.Core/Settings/SettingsManager.About.cs
new file mode 100644
index 0000000..43da6ff
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.About.cs
@@ -0,0 +1,67 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ // 是否接收测试版更新
+ private readonly AllowStatus isReceiveBetaVersion = AllowStatus.NO;
+
+ // 是否在启动时自动检查更新
+ private readonly AllowStatus autoUpdateWhenLaunch = AllowStatus.YES;
+
+ ///
+ /// 获取是否接收测试版更新
+ ///
+ ///
+ public AllowStatus IsReceiveBetaVersion()
+ {
+ appSettings = GetSettings();
+ if (appSettings.About.IsReceiveBetaVersion == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsReceiveBetaVersion(isReceiveBetaVersion);
+ return isReceiveBetaVersion;
+ }
+ return appSettings.About.IsReceiveBetaVersion;
+ }
+
+ ///
+ /// 设置是否接收测试版更新
+ ///
+ ///
+ ///
+ public bool IsReceiveBetaVersion(AllowStatus isReceiveBetaVersion)
+ {
+ appSettings.About.IsReceiveBetaVersion = isReceiveBetaVersion;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否允许启动时检查更新
+ ///
+ ///
+ public AllowStatus GetAutoUpdateWhenLaunch()
+ {
+ appSettings = GetSettings();
+ if (appSettings.About.AutoUpdateWhenLaunch == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ SetAutoUpdateWhenLaunch(autoUpdateWhenLaunch);
+ return autoUpdateWhenLaunch;
+ }
+ return appSettings.About.AutoUpdateWhenLaunch;
+ }
+
+ ///
+ /// 设置是否允许启动时检查更新
+ ///
+ ///
+ ///
+ public bool SetAutoUpdateWhenLaunch(AllowStatus autoUpdateWhenLaunch)
+ {
+ appSettings.About.AutoUpdateWhenLaunch = autoUpdateWhenLaunch;
+ return SetSettings();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.Basic.cs b/src/Downkyi.Core/Settings/SettingsManager.Basic.cs
new file mode 100644
index 0000000..40ca8fc
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.Basic.cs
@@ -0,0 +1,187 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ // 默认下载完成后的操作
+ private readonly AfterDownloadOperation afterDownload = AfterDownloadOperation.NONE;
+
+ // 是否监听剪贴板
+ private readonly AllowStatus isListenClipboard = AllowStatus.YES;
+
+ // 视频详情页面是否自动解析
+ private readonly AllowStatus isAutoParseVideo = AllowStatus.NO;
+
+ // 默认的视频解析项
+ private readonly ParseScope parseScope = ParseScope.NONE;
+
+ // 解析后自动下载解析视频
+ private readonly AllowStatus isAutoDownloadAll = AllowStatus.NO;
+
+ // 下载完成列表排序
+ private readonly DownloadFinishedSort finishedSort = DownloadFinishedSort.DOWNLOAD;
+
+ ///
+ /// 获取下载完成后的操作
+ ///
+ ///
+ public AfterDownloadOperation GetAfterDownloadOperation()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Basic.AfterDownload == AfterDownloadOperation.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetAfterDownloadOperation(afterDownload);
+ return afterDownload;
+ }
+ return appSettings.Basic.AfterDownload;
+ }
+
+ ///
+ /// 设置下载完成后的操作
+ ///
+ ///
+ ///
+ public bool SetAfterDownloadOperation(AfterDownloadOperation afterDownload)
+ {
+ appSettings.Basic.AfterDownload = afterDownload;
+ return SetSettings();
+ }
+
+ ///
+ /// 是否监听剪贴板
+ ///
+ ///
+ public AllowStatus IsListenClipboard()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Basic.IsListenClipboard == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsListenClipboard(isListenClipboard);
+ return isListenClipboard;
+ }
+ return appSettings.Basic.IsListenClipboard;
+ }
+
+ ///
+ /// 是否监听剪贴板
+ ///
+ ///
+ ///
+ public bool IsListenClipboard(AllowStatus isListen)
+ {
+ appSettings.Basic.IsListenClipboard = isListen;
+ return SetSettings();
+ }
+
+ ///
+ /// 视频详情页面是否自动解析
+ ///
+ ///
+ public AllowStatus IsAutoParseVideo()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Basic.IsAutoParseVideo == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsAutoParseVideo(isAutoParseVideo);
+ return isAutoParseVideo;
+ }
+ return appSettings.Basic.IsAutoParseVideo;
+ }
+
+ ///
+ /// 视频详情页面是否自动解析
+ ///
+ ///
+ ///
+ public bool IsAutoParseVideo(AllowStatus IsAuto)
+ {
+ appSettings.Basic.IsAutoParseVideo = IsAuto;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取视频解析项
+ ///
+ ///
+ public ParseScope GetParseScope()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Basic.ParseScope == ParseScope.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetParseScope(parseScope);
+ return parseScope;
+ }
+ return appSettings.Basic.ParseScope;
+ }
+
+ ///
+ /// 设置视频解析项
+ ///
+ ///
+ ///
+ public bool SetParseScope(ParseScope parseScope)
+ {
+ appSettings.Basic.ParseScope = parseScope;
+ return SetSettings();
+ }
+
+ ///
+ /// 解析后是否自动下载解析视频
+ ///
+ ///
+ public AllowStatus IsAutoDownloadAll()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Basic.IsAutoDownloadAll == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsAutoDownloadAll(isAutoDownloadAll);
+ return isAutoDownloadAll;
+ }
+ return appSettings.Basic.IsAutoDownloadAll;
+ }
+
+ ///
+ /// 解析后是否自动下载解析视频
+ ///
+ ///
+ ///
+ public bool IsAutoDownloadAll(AllowStatus isAutoDownloadAll)
+ {
+ appSettings.Basic.IsAutoDownloadAll = isAutoDownloadAll;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取下载完成列表排序
+ ///
+ ///
+ public DownloadFinishedSort GetDownloadFinishedSort()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Basic.DownloadFinishedSort == DownloadFinishedSort.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetDownloadFinishedSort(finishedSort);
+ return finishedSort;
+ }
+ return appSettings.Basic.DownloadFinishedSort;
+ }
+
+ ///
+ /// 设置下载完成列表排序
+ ///
+ ///
+ ///
+ public bool SetDownloadFinishedSort(DownloadFinishedSort finishedSort)
+ {
+ appSettings.Basic.DownloadFinishedSort = finishedSort;
+ return SetSettings();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.Danmaku.cs b/src/Downkyi.Core/Settings/SettingsManager.Danmaku.cs
new file mode 100644
index 0000000..c2db901
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.Danmaku.cs
@@ -0,0 +1,308 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ // 是否屏蔽顶部弹幕
+ private readonly AllowStatus danmakuTopFilter = AllowStatus.NO;
+
+ // 是否屏蔽底部弹幕
+ private readonly AllowStatus danmakuBottomFilter = AllowStatus.NO;
+
+ // 是否屏蔽滚动弹幕
+ private readonly AllowStatus danmakuScrollFilter = AllowStatus.NO;
+
+ // 是否自定义分辨率
+ private readonly AllowStatus isCustomDanmakuResolution = AllowStatus.NO;
+
+ // 分辨率-宽
+ private readonly int danmakuScreenWidth = 1920;
+
+ // 分辨率-高
+ private readonly int danmakuScreenHeight = 1080;
+
+ // 弹幕字体
+ private readonly string danmakuFontName = "黑体";
+
+ // 弹幕字体大小
+ private readonly int danmakuFontSize = 50;
+
+ // 弹幕限制行数
+ private readonly int danmakuLineCount = 0;
+
+ // 弹幕布局算法
+ private readonly DanmakuLayoutAlgorithm danmakuLayoutAlgorithm = DanmakuLayoutAlgorithm.SYNC;
+
+
+ ///
+ /// 获取是否屏蔽顶部弹幕
+ ///
+ ///
+ public AllowStatus GetDanmakuTopFilter()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuTopFilter == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuTopFilter(danmakuTopFilter);
+ return danmakuTopFilter;
+ }
+ return appSettings.Danmaku.DanmakuTopFilter;
+ }
+
+ ///
+ /// 设置是否屏蔽顶部弹幕
+ ///
+ ///
+ ///
+ public bool SetDanmakuTopFilter(AllowStatus danmakuFilter)
+ {
+ appSettings.Danmaku.DanmakuTopFilter = danmakuFilter;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否屏蔽底部弹幕
+ ///
+ ///
+ public AllowStatus GetDanmakuBottomFilter()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuBottomFilter == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuBottomFilter(danmakuBottomFilter);
+ return danmakuBottomFilter;
+ }
+ return appSettings.Danmaku.DanmakuBottomFilter;
+ }
+
+ ///
+ /// 设置是否屏蔽底部弹幕
+ ///
+ ///
+ ///
+ public bool SetDanmakuBottomFilter(AllowStatus danmakuFilter)
+ {
+ appSettings.Danmaku.DanmakuBottomFilter = danmakuFilter;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否屏蔽滚动弹幕
+ ///
+ ///
+ public AllowStatus GetDanmakuScrollFilter()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuScrollFilter == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuScrollFilter(danmakuScrollFilter);
+ return danmakuScrollFilter;
+ }
+ return appSettings.Danmaku.DanmakuScrollFilter;
+ }
+
+ ///
+ /// 设置是否屏蔽滚动弹幕
+ ///
+ ///
+ ///
+ public bool SetDanmakuScrollFilter(AllowStatus danmakuFilter)
+ {
+ appSettings.Danmaku.DanmakuScrollFilter = danmakuFilter;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否自定义分辨率
+ ///
+ ///
+ public AllowStatus IsCustomDanmakuResolution()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.IsCustomDanmakuResolution == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsCustomDanmakuResolution(isCustomDanmakuResolution);
+ return isCustomDanmakuResolution;
+ }
+ return appSettings.Danmaku.IsCustomDanmakuResolution;
+ }
+
+ ///
+ /// 设置是否自定义分辨率
+ ///
+ ///
+ ///
+ public bool IsCustomDanmakuResolution(AllowStatus isCustomResolution)
+ {
+ appSettings.Danmaku.IsCustomDanmakuResolution = isCustomResolution;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取分辨率-宽
+ ///
+ ///
+ public int GetDanmakuScreenWidth()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuScreenWidth == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuScreenWidth(danmakuScreenWidth);
+ return danmakuScreenWidth;
+ }
+ return appSettings.Danmaku.DanmakuScreenWidth;
+ }
+
+ ///
+ /// 设置分辨率-宽
+ ///
+ ///
+ ///
+ public bool SetDanmakuScreenWidth(int screenWidth)
+ {
+ appSettings.Danmaku.DanmakuScreenWidth = screenWidth;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取分辨率-高
+ ///
+ ///
+ public int GetDanmakuScreenHeight()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuScreenHeight == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuScreenHeight(danmakuScreenHeight);
+ return danmakuScreenHeight;
+ }
+ return appSettings.Danmaku.DanmakuScreenHeight;
+ }
+
+ ///
+ /// 设置分辨率-高
+ ///
+ ///
+ ///
+ public bool SetDanmakuScreenHeight(int screenHeight)
+ {
+ appSettings.Danmaku.DanmakuScreenHeight = screenHeight;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取弹幕字体
+ ///
+ ///
+ public string GetDanmakuFontName()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuFontName == null)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuFontName(danmakuFontName);
+ return danmakuFontName;
+ }
+ return appSettings.Danmaku.DanmakuFontName;
+ }
+
+ ///
+ /// 设置弹幕字体
+ ///
+ ///
+ ///
+ public bool SetDanmakuFontName(string danmakuFontName)
+ {
+ appSettings.Danmaku.DanmakuFontName = danmakuFontName;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取弹幕字体大小
+ ///
+ ///
+ public int GetDanmakuFontSize()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuFontSize == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuFontSize(danmakuFontSize);
+ return danmakuFontSize;
+ }
+ return appSettings.Danmaku.DanmakuFontSize;
+ }
+
+ ///
+ /// 设置弹幕字体大小
+ ///
+ ///
+ ///
+ public bool SetDanmakuFontSize(int danmakuFontSize)
+ {
+ appSettings.Danmaku.DanmakuFontSize = danmakuFontSize;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取弹幕限制行数
+ ///
+ ///
+ public int GetDanmakuLineCount()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuLineCount == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuLineCount(danmakuLineCount);
+ return danmakuLineCount;
+ }
+ return appSettings.Danmaku.DanmakuLineCount;
+ }
+
+ ///
+ /// 设置弹幕限制行数
+ ///
+ ///
+ ///
+ public bool SetDanmakuLineCount(int danmakuLineCount)
+ {
+ appSettings.Danmaku.DanmakuLineCount = danmakuLineCount;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取弹幕布局算法
+ ///
+ ///
+ public DanmakuLayoutAlgorithm GetDanmakuLayoutAlgorithm()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Danmaku.DanmakuLayoutAlgorithm == DanmakuLayoutAlgorithm.NONE)
+ {
+ // 第一次获取,先设置默认值
+ SetDanmakuLayoutAlgorithm(danmakuLayoutAlgorithm);
+ return danmakuLayoutAlgorithm;
+ }
+ return appSettings.Danmaku.DanmakuLayoutAlgorithm;
+ }
+
+ ///
+ /// 设置弹幕布局算法
+ ///
+ ///
+ ///
+ public bool SetDanmakuLayoutAlgorithm(DanmakuLayoutAlgorithm danmakuLayoutAlgorithm)
+ {
+ appSettings.Danmaku.DanmakuLayoutAlgorithm = danmakuLayoutAlgorithm;
+ return SetSettings();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.Network.cs b/src/Downkyi.Core/Settings/SettingsManager.Network.cs
new file mode 100644
index 0000000..186eb8f
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.Network.cs
@@ -0,0 +1,600 @@
+using Aria2cNet.Server;
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ // 是否开启解除地区限制
+ private readonly AllowStatus isLiftingOfRegion = AllowStatus.YES;
+
+ // 启用https
+ private readonly AllowStatus useSSL = AllowStatus.YES;
+
+ // UserAgent
+ private readonly string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
+
+ // 下载器
+ private readonly Enum.Downloader downloader = Enum.Downloader.BUILT_IN;
+
+ // 最大同时下载数(任务数)
+ private readonly int maxCurrentDownloads = 3;
+
+ // 单文件最大线程数
+ private readonly int split = 8;
+
+ // HttpProxy代理
+ private readonly AllowStatus isHttpProxy = AllowStatus.NO;
+ private readonly string httpProxy = string.Empty;
+ private readonly int httpProxyListenPort = 0;
+
+ // Aria服务器token
+ private readonly string ariaToken = "downkyi";
+
+ // Aria服务器host
+ private readonly string ariaHost = "http://localhost";
+
+ // Aria服务器端口号
+ private readonly int ariaListenPort = 6800;
+
+ // Aria日志等级
+ private readonly AriaConfigLogLevel ariaLogLevel = AriaConfigLogLevel.INFO;
+
+ // Aria单文件最大线程数
+ private readonly int ariaSplit = 5;
+
+ // Aria下载速度限制
+ private readonly int ariaMaxOverallDownloadLimit = 0;
+
+ // Aria下载单文件速度限制
+ private readonly int ariaMaxDownloadLimit = 0;
+
+ // Aria文件预分配
+ private readonly AriaConfigFileAllocation ariaFileAllocation = AriaConfigFileAllocation.NONE;
+
+ // Aria HttpProxy代理
+ private readonly AllowStatus isAriaHttpProxy = AllowStatus.NO;
+ private readonly string ariaHttpProxy = string.Empty;
+ private readonly int ariaHttpProxyListenPort = 0;
+
+ ///
+ /// 获取是否解除地区限制
+ ///
+ ///
+ public AllowStatus IsLiftingOfRegion()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.IsLiftingOfRegion == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsLiftingOfRegion(isLiftingOfRegion);
+ return isLiftingOfRegion;
+ }
+ return appSettings.Network.IsLiftingOfRegion;
+ }
+
+ ///
+ /// 设置是否解除地区限制
+ ///
+ ///
+ ///
+ public bool IsLiftingOfRegion(AllowStatus isLiftingOfRegion)
+ {
+ appSettings.Network.IsLiftingOfRegion = isLiftingOfRegion;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否启用https
+ ///
+ ///
+ public AllowStatus UseSSL()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.UseSSL == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ UseSSL(useSSL);
+ return useSSL;
+ }
+ return appSettings.Network.UseSSL;
+ }
+
+ ///
+ /// 设置是否启用https
+ ///
+ ///
+ ///
+ public bool UseSSL(AllowStatus useSSL)
+ {
+ appSettings.Network.UseSSL = useSSL;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取UserAgent
+ ///
+ ///
+ public string GetUserAgent()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.UserAgent == string.Empty)
+ {
+ // 第一次获取,先设置默认值
+ SetUserAgent(userAgent);
+ return userAgent;
+ }
+ return appSettings.Network.UserAgent;
+ }
+
+ ///
+ /// 设置UserAgent
+ ///
+ ///
+ ///
+ public bool SetUserAgent(string userAgent)
+ {
+ appSettings.Network.UserAgent = userAgent;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取下载器
+ ///
+ ///
+ public Enum.Downloader GetDownloader()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.Downloader == Enum.Downloader.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetDownloader(downloader);
+ return downloader;
+ }
+ return appSettings.Network.Downloader;
+ }
+
+ ///
+ /// 设置下载器
+ ///
+ ///
+ ///
+ public bool SetDownloader(Enum.Downloader downloader)
+ {
+ appSettings.Network.Downloader = downloader;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取最大同时下载数(任务数)
+ ///
+ ///
+ public int GetMaxCurrentDownloads()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.MaxCurrentDownloads == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetMaxCurrentDownloads(maxCurrentDownloads);
+ return maxCurrentDownloads;
+ }
+ return appSettings.Network.MaxCurrentDownloads;
+ }
+
+ ///
+ /// 设置最大同时下载数(任务数)
+ ///
+ ///
+ ///
+ public bool SetMaxCurrentDownloads(int maxCurrentDownloads)
+ {
+ appSettings.Network.MaxCurrentDownloads = maxCurrentDownloads;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取单文件最大线程数
+ ///
+ ///
+ public int GetSplit()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.Split == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetSplit(split);
+ return split;
+ }
+ return appSettings.Network.Split;
+ }
+
+ ///
+ /// 设置单文件最大线程数
+ ///
+ ///
+ ///
+ public bool SetSplit(int split)
+ {
+ appSettings.Network.Split = split;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否开启Http代理
+ ///
+ ///
+ public AllowStatus IsHttpProxy()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.IsHttpProxy == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsHttpProxy(isHttpProxy);
+ return isHttpProxy;
+ }
+ return appSettings.Network.IsHttpProxy;
+ }
+
+ ///
+ /// 设置是否开启Http代理
+ ///
+ ///
+ ///
+ public bool IsHttpProxy(AllowStatus isHttpProxy)
+ {
+ appSettings.Network.IsHttpProxy = isHttpProxy;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Http代理的地址
+ ///
+ ///
+ public string GetHttpProxy()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.HttpProxy == null)
+ {
+ // 第一次获取,先设置默认值
+ SetHttpProxy(httpProxy);
+ return httpProxy;
+ }
+ return appSettings.Network.HttpProxy;
+ }
+
+ ///
+ /// 设置Aria的http代理的地址
+ ///
+ ///
+ ///
+ public bool SetHttpProxy(string httpProxy)
+ {
+ appSettings.Network.HttpProxy = httpProxy;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Http代理的端口
+ ///
+ ///
+ public int GetHttpProxyListenPort()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.HttpProxyListenPort == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetHttpProxyListenPort(httpProxyListenPort);
+ return httpProxyListenPort;
+ }
+ return appSettings.Network.HttpProxyListenPort;
+ }
+
+ ///
+ /// 设置Http代理的端口
+ ///
+ ///
+ ///
+ public bool SetHttpProxyListenPort(int httpProxyListenPort)
+ {
+ appSettings.Network.HttpProxyListenPort = httpProxyListenPort;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria服务器的token
+ ///
+ ///
+ public string GetAriaToken()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaToken == null)
+ {
+ // 第一次获取,先设置默认值
+ SetHttpProxy(ariaToken);
+ return ariaToken;
+ }
+ return appSettings.Network.AriaToken;
+ }
+
+ ///
+ /// 设置Aria服务器的token
+ ///
+ ///
+ ///
+ public bool SetAriaToken(string token)
+ {
+ appSettings.Network.AriaToken = token;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria服务器的host
+ ///
+ ///
+ public string GetAriaHost()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaHost == null)
+ {
+ // 第一次获取,先设置默认值
+ SetHttpProxy(ariaHost);
+ return ariaHost;
+ }
+ return appSettings.Network.AriaHost;
+ }
+
+ ///
+ /// 设置Aria服务器的host
+ ///
+ ///
+ ///
+ public bool SetAriaHost(string host)
+ {
+ appSettings.Network.AriaHost = host;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria服务器的端口号
+ ///
+ ///
+ public int GetAriaListenPort()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaListenPort == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaListenPort(ariaListenPort);
+ return ariaListenPort;
+ }
+ return appSettings.Network.AriaListenPort;
+ }
+
+ ///
+ /// 设置Aria服务器的端口号
+ ///
+ ///
+ ///
+ public bool SetAriaListenPort(int ariaListenPort)
+ {
+ appSettings.Network.AriaListenPort = ariaListenPort;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria日志等级
+ ///
+ ///
+ public AriaConfigLogLevel GetAriaLogLevel()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaLogLevel == AriaConfigLogLevel.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaLogLevel(ariaLogLevel);
+ return ariaLogLevel;
+ }
+ return appSettings.Network.AriaLogLevel;
+ }
+
+ ///
+ /// 设置Aria日志等级
+ ///
+ ///
+ ///
+ public bool SetAriaLogLevel(AriaConfigLogLevel ariaLogLevel)
+ {
+ appSettings.Network.AriaLogLevel = ariaLogLevel;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria单文件最大线程数
+ ///
+ ///
+ public int GetAriaSplit()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaSplit == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaSplit(ariaSplit);
+ return ariaSplit;
+ }
+ return appSettings.Network.AriaSplit;
+ }
+
+ ///
+ /// 设置Aria单文件最大线程数
+ ///
+ ///
+ ///
+ public bool SetAriaSplit(int ariaSplit)
+ {
+ appSettings.Network.AriaSplit = ariaSplit;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria下载速度限制
+ ///
+ ///
+ public int GetAriaMaxOverallDownloadLimit()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaMaxOverallDownloadLimit == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaMaxOverallDownloadLimit(ariaMaxOverallDownloadLimit);
+ return ariaMaxOverallDownloadLimit;
+ }
+ return appSettings.Network.AriaMaxOverallDownloadLimit;
+ }
+
+ ///
+ /// 设置Aria下载速度限制
+ ///
+ ///
+ ///
+ public bool SetAriaMaxOverallDownloadLimit(int ariaMaxOverallDownloadLimit)
+ {
+ appSettings.Network.AriaMaxOverallDownloadLimit = ariaMaxOverallDownloadLimit;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria下载单文件速度限制
+ ///
+ ///
+ public int GetAriaMaxDownloadLimit()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaMaxDownloadLimit == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaMaxDownloadLimit(ariaMaxDownloadLimit);
+ return ariaMaxDownloadLimit;
+ }
+ return appSettings.Network.AriaMaxDownloadLimit;
+ }
+
+ ///
+ /// 设置Aria下载单文件速度限制
+ ///
+ ///
+ ///
+ public bool SetAriaMaxDownloadLimit(int ariaMaxDownloadLimit)
+ {
+ appSettings.Network.AriaMaxDownloadLimit = ariaMaxDownloadLimit;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria文件预分配
+ ///
+ ///
+ public AriaConfigFileAllocation GetAriaFileAllocation()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaFileAllocation == AriaConfigFileAllocation.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaFileAllocation(ariaFileAllocation);
+ return ariaFileAllocation;
+ }
+ return appSettings.Network.AriaFileAllocation;
+ }
+
+ ///
+ /// 设置Aria文件预分配
+ ///
+ ///
+ ///
+ public bool SetAriaFileAllocation(AriaConfigFileAllocation ariaFileAllocation)
+ {
+ appSettings.Network.AriaFileAllocation = ariaFileAllocation;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否开启Aria http代理
+ ///
+ ///
+ public AllowStatus IsAriaHttpProxy()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.IsAriaHttpProxy == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsAriaHttpProxy(isAriaHttpProxy);
+ return isAriaHttpProxy;
+ }
+ return appSettings.Network.IsAriaHttpProxy;
+ }
+
+ ///
+ /// 设置是否开启Aria http代理
+ ///
+ ///
+ ///
+ public bool IsAriaHttpProxy(AllowStatus isAriaHttpProxy)
+ {
+ appSettings.Network.IsAriaHttpProxy = isAriaHttpProxy;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria的http代理的地址
+ ///
+ ///
+ public string GetAriaHttpProxy()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaHttpProxy == null)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaHttpProxy(ariaHttpProxy);
+ return ariaHttpProxy;
+ }
+ return appSettings.Network.AriaHttpProxy;
+ }
+
+ ///
+ /// 设置Aria的http代理的地址
+ ///
+ ///
+ ///
+ public bool SetAriaHttpProxy(string ariaHttpProxy)
+ {
+ appSettings.Network.AriaHttpProxy = ariaHttpProxy;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取Aria的http代理的端口
+ ///
+ ///
+ public int GetAriaHttpProxyListenPort()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Network.AriaHttpProxyListenPort == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetAriaHttpProxyListenPort(ariaHttpProxyListenPort);
+ return ariaHttpProxyListenPort;
+ }
+ return appSettings.Network.AriaHttpProxyListenPort;
+ }
+
+ ///
+ /// 设置Aria的http代理的端口
+ ///
+ ///
+ ///
+ public bool SetAriaHttpProxyListenPort(int ariaHttpProxyListenPort)
+ {
+ appSettings.Network.AriaHttpProxyListenPort = ariaHttpProxyListenPort;
+ return SetSettings();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.UserInfo.cs b/src/Downkyi.Core/Settings/SettingsManager.UserInfo.cs
new file mode 100644
index 0000000..267b8f6
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.UserInfo.cs
@@ -0,0 +1,43 @@
+using Downkyi.Core.Settings.Models;
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ // 登录用户的mid
+ private readonly UserInfoSettings userInfo = new()
+ {
+ Mid = -1,
+ Name = "",
+ IsLogin = false,
+ IsVip = false
+ };
+
+ ///
+ /// 获取登录用户信息
+ ///
+ ///
+ public UserInfoSettings GetUserInfo()
+ {
+ appSettings = GetSettings();
+ if (appSettings.UserInfo == null)
+ {
+ // 第一次获取,先设置默认值
+ SetUserInfo(userInfo);
+ return userInfo;
+ }
+ return appSettings.UserInfo;
+ }
+
+ ///
+ /// 设置中保存登录用户的信息,在index刷新用户状态时使用
+ ///
+ ///
+ ///
+ public bool SetUserInfo(UserInfoSettings userInfo)
+ {
+ appSettings.UserInfo = userInfo;
+ return SetSettings();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.Video.cs b/src/Downkyi.Core/Settings/SettingsManager.Video.cs
new file mode 100644
index 0000000..f543ca2
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.Video.cs
@@ -0,0 +1,352 @@
+using Downkyi.Core.FileName;
+using Downkyi.Core.Settings.Enum;
+using Downkyi.Core.Settings.Models;
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ // 设置优先下载的视频编码
+ private readonly int videoCodecs = 7;
+
+ // 设置优先下载画质
+ private readonly int quality = 120;
+
+ // 设置优先下载音质
+ private readonly int audioQuality = 30280;
+
+ // 是否下载flv视频后转码为mp4
+ private readonly AllowStatus isTranscodingFlvToMp4 = AllowStatus.YES;
+
+ // 默认下载目录
+ private readonly string saveVideoRootPath = Path.Combine(Environment.CurrentDirectory, "Media");
+
+ // 历史下载目录
+ private readonly List historyVideoRootPaths = new();
+
+ // 是否使用默认下载目录,如果是,则每次点击下载选中项时不再询问下载目录
+ private readonly AllowStatus isUseSaveVideoRootPath = AllowStatus.NO;
+
+ // 下载内容
+ private readonly VideoContentSettings videoContent = new();
+
+ // 文件命名格式
+ private readonly List fileNameParts = new()
+ {
+ FileNamePart.MAIN_TITLE,
+ FileNamePart.SLASH,
+ FileNamePart.SECTION,
+ FileNamePart.SLASH,
+ FileNamePart.ORDER,
+ FileNamePart.HYPHEN,
+ FileNamePart.PAGE_TITLE,
+ FileNamePart.HYPHEN,
+ FileNamePart.VIDEO_QUALITY,
+ FileNamePart.HYPHEN,
+ FileNamePart.VIDEO_CODEC,
+ };
+
+ // 文件命名中的时间格式
+ private readonly string fileNamePartTimeFormat = "yyyy-MM-dd";
+
+ // 文件命名中的序号格式
+ private readonly OrderFormat orderFormat = OrderFormat.NATURAL;
+
+ ///
+ /// 获取优先下载的视频编码
+ ///
+ ///
+ public int GetVideoCodecs()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.VideoCodecs == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetVideoCodecs(videoCodecs);
+ return videoCodecs;
+ }
+ return appSettings.Video.VideoCodecs;
+ }
+
+ ///
+ /// 设置优先下载的视频编码
+ ///
+ ///
+ ///
+ public bool SetVideoCodecs(int videoCodecs)
+ {
+ appSettings.Video.VideoCodecs = videoCodecs;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取优先下载画质
+ ///
+ ///
+ public int GetQuality()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.Quality == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetQuality(quality);
+ return quality;
+ }
+ return appSettings.Video.Quality;
+ }
+
+ ///
+ /// 设置优先下载画质
+ ///
+ ///
+ ///
+ public bool SetQuality(int quality)
+ {
+ appSettings.Video.Quality = quality;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取优先下载音质
+ ///
+ ///
+ public int GetAudioQuality()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.AudioQuality == -1)
+ {
+ // 第一次获取,先设置默认值
+ SetAudioQuality(audioQuality);
+ return audioQuality;
+ }
+ return appSettings.Video.AudioQuality;
+ }
+
+ ///
+ /// 设置优先下载音质
+ ///
+ ///
+ ///
+ public bool SetAudioQuality(int quality)
+ {
+ appSettings.Video.AudioQuality = quality;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否下载flv视频后转码为mp4
+ ///
+ ///
+ public AllowStatus IsTranscodingFlvToMp4()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.IsTranscodingFlvToMp4 == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsTranscodingFlvToMp4(isTranscodingFlvToMp4);
+ return isTranscodingFlvToMp4;
+ }
+ return appSettings.Video.IsTranscodingFlvToMp4;
+ }
+
+ ///
+ /// 设置是否下载flv视频后转码为mp4
+ ///
+ ///
+ ///
+ public bool IsTranscodingFlvToMp4(AllowStatus isTranscodingFlvToMp4)
+ {
+ appSettings.Video.IsTranscodingFlvToMp4 = isTranscodingFlvToMp4;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取下载目录
+ ///
+ ///
+ public string GetSaveVideoRootPath()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.SaveVideoRootPath == null)
+ {
+ // 第一次获取,先设置默认值
+ SetSaveVideoRootPath(saveVideoRootPath);
+ return saveVideoRootPath;
+ }
+ return appSettings.Video.SaveVideoRootPath;
+ }
+
+ ///
+ /// 设置下载目录
+ ///
+ ///
+ ///
+ public bool SetSaveVideoRootPath(string path)
+ {
+ appSettings.Video.SaveVideoRootPath = path;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取历史下载目录
+ ///
+ ///
+ public List GetHistoryVideoRootPaths()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.HistoryVideoRootPaths == null)
+ {
+ // 第一次获取,先设置默认值
+ SetHistoryVideoRootPaths(historyVideoRootPaths);
+ return historyVideoRootPaths;
+ }
+ return appSettings.Video.HistoryVideoRootPaths;
+ }
+
+ ///
+ /// 设置历史下载目录
+ ///
+ ///
+ ///
+ public bool SetHistoryVideoRootPaths(List historyPaths)
+ {
+ appSettings.Video.HistoryVideoRootPaths = historyPaths;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取是否使用默认下载目录
+ ///
+ ///
+ public AllowStatus IsUseSaveVideoRootPath()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.IsUseSaveVideoRootPath == AllowStatus.NONE)
+ {
+ // 第一次获取,先设置默认值
+ IsUseSaveVideoRootPath(isUseSaveVideoRootPath);
+ return isUseSaveVideoRootPath;
+ }
+ return appSettings.Video.IsUseSaveVideoRootPath;
+ }
+
+ ///
+ /// 设置是否使用默认下载目录
+ ///
+ ///
+ ///
+ public bool IsUseSaveVideoRootPath(AllowStatus isUseSaveVideoRootPath)
+ {
+ appSettings.Video.IsUseSaveVideoRootPath = isUseSaveVideoRootPath;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取下载内容
+ ///
+ ///
+ public VideoContentSettings GetVideoContent()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.VideoContent == null)
+ {
+ // 第一次获取,先设置默认值
+ SetVideoContent(videoContent);
+ return videoContent;
+ }
+ return appSettings.Video.VideoContent;
+ }
+
+ ///
+ /// 设置下载内容
+ ///
+ ///
+ ///
+ public bool SetVideoContent(VideoContentSettings videoContent)
+ {
+ appSettings.Video.VideoContent = videoContent;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取文件命名格式
+ ///
+ ///
+ public List GetFileNameParts()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.FileNameParts == null || appSettings.Video.FileNameParts.Count == 0)
+ {
+ // 第一次获取,先设置默认值
+ SetFileNameParts(fileNameParts);
+ return fileNameParts;
+ }
+ return appSettings.Video.FileNameParts;
+ }
+
+ ///
+ /// 设置文件命名格式
+ ///
+ ///
+ ///
+ public bool SetFileNameParts(List fileNameParts)
+ {
+ appSettings.Video.FileNameParts = fileNameParts;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取文件命名中的时间格式
+ ///
+ ///
+ public string GetFileNamePartTimeFormat()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.FileNamePartTimeFormat == null || appSettings.Video.FileNamePartTimeFormat == string.Empty)
+ {
+ // 第一次获取,先设置默认值
+ SetFileNamePartTimeFormat(fileNamePartTimeFormat);
+ return fileNamePartTimeFormat;
+ }
+ return appSettings.Video.FileNamePartTimeFormat;
+ }
+
+ ///
+ /// 设置文件命名中的时间格式
+ ///
+ ///
+ ///
+ public bool SetFileNamePartTimeFormat(string timeFormat)
+ {
+ appSettings.Video.FileNamePartTimeFormat = timeFormat;
+ return SetSettings();
+ }
+
+ ///
+ /// 获取文件命名中的序号格式
+ ///
+ ///
+ public OrderFormat GetOrderFormat()
+ {
+ appSettings = GetSettings();
+ if (appSettings.Video.OrderFormat == OrderFormat.NOT_SET)
+ {
+ // 第一次获取,先设置默认值
+ SetOrderFormat(orderFormat);
+ return orderFormat;
+ }
+ return appSettings.Video.OrderFormat;
+ }
+
+ ///
+ /// 设置文件命名中的序号格式
+ ///
+ ///
+ ///
+ public bool SetOrderFormat(OrderFormat orderFormat)
+ {
+ appSettings.Video.OrderFormat = orderFormat;
+ return SetSettings();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Settings/SettingsManager.cs b/src/Downkyi.Core/Settings/SettingsManager.cs
new file mode 100644
index 0000000..6716091
--- /dev/null
+++ b/src/Downkyi.Core/Settings/SettingsManager.cs
@@ -0,0 +1,102 @@
+using Downkyi.Core.Settings.Models;
+using Newtonsoft.Json;
+
+#if DEBUG
+#else
+using Downkyi.Core.Utils.Encryptor;
+#endif
+
+namespace Downkyi.Core.Settings;
+
+public partial class SettingsManager
+{
+ private static SettingsManager instance;
+
+ // 内存中保存一份配置
+ private AppSettings appSettings;
+
+#if DEBUG
+ // 设置的配置文件
+ private readonly string settingsName = Storage.StorageManager.GetSettings() + "_debug.json";
+#else
+ // 设置的配置文件
+ private readonly string settingsName = Storage.StorageManager.GetSettings();
+
+ // 密钥
+ private readonly string password = "YO1J$4#p";
+#endif
+
+ ///
+ /// 获取SettingsManager实例
+ ///
+ ///
+ public static SettingsManager GetInstance()
+ {
+ instance ??= new SettingsManager();
+ return instance;
+ }
+
+ ///
+ /// 隐藏Settings()方法,必须使用单例模式
+ ///
+ private SettingsManager()
+ {
+ appSettings = GetSettings();
+ }
+
+ ///
+ /// 获取AppSettingsModel
+ ///
+ ///
+ private AppSettings GetSettings()
+ {
+ try
+ {
+ var fileStream = new FileStream(settingsName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ var streamReader = new StreamReader(fileStream, System.Text.Encoding.UTF8);
+ string jsonWordTemplate = streamReader.ReadToEnd();
+ streamReader.Close();
+ fileStream.Close();
+
+#if DEBUG
+#else
+ // 解密字符串
+ jsonWordTemplate = Encryptor.DecryptString(jsonWordTemplate, password);
+#endif
+
+ return JsonConvert.DeserializeObject(jsonWordTemplate);
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return new AppSettings();
+ }
+ }
+
+ ///
+ /// 设置AppSettingsModel
+ ///
+ ///
+ private bool SetSettings()
+ {
+ try
+ {
+ string json = JsonConvert.SerializeObject(appSettings);
+
+#if DEBUG
+#else
+ // 加密字符串
+ json = Encryptor.EncryptString(json, password);
+#endif
+
+ File.WriteAllText(settingsName, json);
+ return true;
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Storage/Constant.cs b/src/Downkyi.Core/Storage/Constant.cs
new file mode 100644
index 0000000..9b2a07c
--- /dev/null
+++ b/src/Downkyi.Core/Storage/Constant.cs
@@ -0,0 +1,67 @@
+namespace Downkyi.Core.Storage;
+
+///
+/// 存储到本地时使用的一些常量
+///
+internal static class Constant
+{
+ // 根目录
+ //private static string Root { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/Downkyi";
+ private static string Root { get; } = Environment.CurrentDirectory;
+
+ // Aria
+ public static string Aria { get; } = $"{Root}/Aria";
+
+ // 日志
+ public static string Logs { get; } = $"{Root}/Logs";
+
+ // 数据库
+ public static string Database { get; } = $"{Root}/Storage";
+
+ // 历史(搜索、下载) (加密)
+ public static string Download { get; } = $"{Database}/Download.db";
+ public static string History { get; } = $"{Database}/History.db";
+
+ // 配置
+ public static string Config { get; } = $"{Root}/Config";
+
+ // 设置
+ public static string Settings { get; } = $"{Config}/Settings";
+
+ // 登录cookies
+ public static string Login { get; } = $"{Config}/Login";
+
+ // Bilibili
+ private static string Bilibili { get; } = $"{Root}/Bilibili";
+
+ // 弹幕
+ public static string Danmaku { get; } = $"{Bilibili}/Danmakus";
+
+ // 字幕
+ public static string Subtitle { get; } = $"{Bilibili}/Subtitle";
+
+ // 评论
+ // TODO
+
+ // 头图
+ public static string Toutu { get; } = $"{Bilibili}/Toutu";
+
+ // 封面
+ public static string Cover { get; } = $"{Bilibili}/Cover";
+
+ // 封面文件索引
+ public static string CoverIndex { get; } = $"{Cover}/Index.db";
+
+ // 视频快照
+ public static string Snapshot { get; } = $"{Bilibili}/Snapshot";
+
+ // 视频快照文件索引
+ public static string SnapshotIndex { get; } = $"{Cover}/Index.db";
+
+ // 用户头像
+ public static string Header { get; } = $"{Bilibili}/Header";
+
+ // 用户头像文件索引
+ public static string HeaderIndex { get; } = $"{Header}/Index.db";
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Storage/StorageManager.cs b/src/Downkyi.Core/Storage/StorageManager.cs
new file mode 100644
index 0000000..681aa4f
--- /dev/null
+++ b/src/Downkyi.Core/Storage/StorageManager.cs
@@ -0,0 +1,153 @@
+namespace Downkyi.Core.Storage;
+
+public static class StorageManager
+{
+ ///
+ /// 获取历史记录的文件路径
+ ///
+ ///
+ public static string GetAriaDir()
+ {
+ CreateDirectory(Constant.Aria);
+ return Constant.Aria;
+ }
+
+ ///
+ /// 获取历史记录的文件路径
+ ///
+ ///
+ public static string GetDownload()
+ {
+ CreateDirectory(Constant.Database);
+ return Constant.Download;
+ }
+
+ ///
+ /// 获取历史记录的文件路径
+ ///
+ ///
+ public static string GetHistory()
+ {
+ CreateDirectory(Constant.Database);
+ return Constant.History;
+ }
+
+ ///
+ /// 获取设置的文件路径
+ ///
+ ///
+ public static string GetSettings()
+ {
+ CreateDirectory(Constant.Config);
+ return Constant.Settings;
+ }
+
+ ///
+ /// 获取登录cookies的文件路径
+ ///
+ ///
+ public static string GetLogin()
+ {
+ CreateDirectory(Constant.Config);
+ return Constant.Login;
+ }
+
+ ///
+ /// 获取弹幕的文件夹路径
+ ///
+ ///
+ public static string GetDanmaku()
+ {
+ return CreateDirectory(Constant.Danmaku);
+ }
+
+ ///
+ /// 获取字幕的文件夹路径
+ ///
+ ///
+ public static string GetSubtitle()
+ {
+ return CreateDirectory(Constant.Subtitle);
+ }
+
+ ///
+ /// 获取头图的文件夹路径
+ ///
+ ///
+ public static string GetToutu()
+ {
+ return CreateDirectory(Constant.Toutu);
+ }
+
+ ///
+ /// 获取封面的文件夹路径
+ ///
+ ///
+ public static string GetCover()
+ {
+ return CreateDirectory(Constant.Cover);
+ }
+
+ ///
+ /// 获取封面索引的文件路径
+ ///
+ ///
+ public static string GetCoverIndex()
+ {
+ CreateDirectory(Constant.Cover);
+ return Constant.CoverIndex;
+ }
+
+ ///
+ /// 获取视频快照的文件夹路径
+ ///
+ ///
+ public static string GetSnapshot()
+ {
+ return CreateDirectory(Constant.Snapshot);
+ }
+
+ ///
+ /// 获取视频快照索引的文件路径
+ ///
+ ///
+ public static string GetSnapshotIndex()
+ {
+ CreateDirectory(Constant.Snapshot);
+ return Constant.SnapshotIndex;
+ }
+
+ ///
+ /// 获取用户头像的文件夹路径
+ ///
+ ///
+ public static string GetHeader()
+ {
+ return CreateDirectory(Constant.Header);
+ }
+
+ ///
+ /// 获取用户头像索引的文件路径
+ ///
+ ///
+ public static string GetHeaderIndex()
+ {
+ CreateDirectory(Constant.Header);
+ return Constant.HeaderIndex;
+ }
+
+
+ ///
+ /// 若文件夹不存在,则创建文件夹
+ ///
+ ///
+ ///
+ private static string CreateDirectory(string directory)
+ {
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ return directory;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/Encryptor/Encryptor.File.cs b/src/Downkyi.Core/Utils/Encryptor/Encryptor.File.cs
new file mode 100644
index 0000000..15b9ec6
--- /dev/null
+++ b/src/Downkyi.Core/Utils/Encryptor/Encryptor.File.cs
@@ -0,0 +1,185 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Downkyi.Core.Utils.Encryptor;
+
+public sealed class EncryptorFile
+{
+ string iv = "xIrlMBGX";
+ string key = "A2&P!buv";
+
+ ///
+ /// DES加密偏移量,必须是>=8位长的字符串
+ ///
+ public string IV
+ {
+ get { return iv; }
+ set { iv = value; }
+ }
+
+ ///
+ /// DES加密的私钥,必须是8位长的字符串
+ ///
+ public string Key
+ {
+ get { return key; }
+ set { key = value; }
+ }
+
+ ///
+ /// 对字符串进行DES加密
+ ///
+ /// 待加密的字符串
+ /// 加密后的BASE64编码的字符串
+ public string Encrypt(string sourceString)
+ {
+ byte[] btKey = Encoding.Default.GetBytes(key);
+ byte[] btIV = Encoding.Default.GetBytes(iv);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ var des = new DESCryptoServiceProvider();
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+ using (MemoryStream ms = new MemoryStream())
+ {
+ byte[] inData = Encoding.Default.GetBytes(sourceString);
+ try
+ {
+ using (CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(btKey, btIV), CryptoStreamMode.Write))
+ {
+ cs.Write(inData, 0, inData.Length);
+ cs.FlushFinalBlock();
+ }
+
+ return Convert.ToBase64String(ms.ToArray());
+ }
+ catch
+ {
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// 对DES加密后的字符串进行解密
+ ///
+ /// 待解密的字符串
+ /// 解密后的字符串
+ public string Decrypt(string encryptedString)
+ {
+ byte[] btKey = Encoding.Default.GetBytes(key);
+ byte[] btIV = Encoding.Default.GetBytes(iv);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ var des = new DESCryptoServiceProvider();
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+
+ using (MemoryStream ms = new MemoryStream())
+ {
+ byte[] inData = Convert.FromBase64String(encryptedString);
+ try
+ {
+ using (CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(btKey, btIV), CryptoStreamMode.Write))
+ {
+ cs.Write(inData, 0, inData.Length);
+ cs.FlushFinalBlock();
+ }
+
+ return Encoding.Default.GetString(ms.ToArray());
+ }
+ catch
+ {
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// 对文件内容进行DES加密
+ ///
+ /// 待加密的文件绝对路径
+ /// 加密后的文件保存的绝对路径
+ public void EncryptFile(string sourceFile, string destFile)
+ {
+ if (!File.Exists(sourceFile)) throw new FileNotFoundException("指定的文件路径不存在!", sourceFile);
+
+ byte[] btKey = Encoding.Default.GetBytes(key);
+ byte[] btIV = Encoding.Default.GetBytes(iv);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ var des = new DESCryptoServiceProvider();
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+ byte[] btFile = File.ReadAllBytes(sourceFile);
+
+ using (FileStream fs = new FileStream(destFile, FileMode.Create, FileAccess.Write))
+ {
+ try
+ {
+ using (CryptoStream cs = new CryptoStream(fs, des.CreateEncryptor(btKey, btIV), CryptoStreamMode.Write))
+ {
+ cs.Write(btFile, 0, btFile.Length);
+ cs.FlushFinalBlock();
+ }
+ }
+ catch
+ {
+ throw;
+ }
+ finally
+ {
+ fs.Close();
+ }
+ }
+ }
+
+ ///
+ /// 对文件内容进行DES加密,加密后覆盖掉原来的文件
+ ///
+ /// 待加密的文件的绝对路径
+ public void EncryptFile(string sourceFile)
+ {
+ EncryptFile(sourceFile, sourceFile);
+ }
+
+ ///
+ /// 对文件内容进行DES解密
+ ///
+ /// 待解密的文件绝对路径
+ /// 解密后的文件保存的绝对路径
+ public void DecryptFile(string sourceFile, string destFile)
+ {
+ if (!File.Exists(sourceFile)) throw new FileNotFoundException("指定的文件路径不存在!", sourceFile);
+
+ byte[] btKey = Encoding.Default.GetBytes(key);
+ byte[] btIV = Encoding.Default.GetBytes(iv);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ var des = new DESCryptoServiceProvider();
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+ byte[] btFile = File.ReadAllBytes(sourceFile);
+
+ using (FileStream fs = new FileStream(destFile, FileMode.Create, FileAccess.Write))
+ {
+ try
+ {
+ using (CryptoStream cs = new CryptoStream(fs, des.CreateDecryptor(btKey, btIV), CryptoStreamMode.Write))
+ {
+ cs.Write(btFile, 0, btFile.Length);
+ cs.FlushFinalBlock();
+ }
+ }
+ catch
+ {
+ throw;
+ }
+ finally
+ {
+ fs.Close();
+ }
+ }
+ }
+
+ ///
+ /// 对文件内容进行DES解密,加密后覆盖掉原来的文件
+ ///
+ /// 待解密的文件的绝对路径
+ public void DecryptFile(string sourceFile)
+ {
+ DecryptFile(sourceFile, sourceFile);
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/Encryptor/Encryptor.String.cs b/src/Downkyi.Core/Utils/Encryptor/Encryptor.String.cs
new file mode 100644
index 0000000..e3ae690
--- /dev/null
+++ b/src/Downkyi.Core/Utils/Encryptor/Encryptor.String.cs
@@ -0,0 +1,68 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Downkyi.Core.Utils.Encryptor;
+
+public static partial class Encryptor
+{
+ ///
+ /// DES加密字符串
+ ///
+ /// 待加密的字符串
+ /// 加密密钥,要求为8位
+ /// 加密成功返回加密后的字符串,失败返回源串
+ public static string EncryptString(string encryptString, string encryptKey)
+ {
+ try
+ {
+ byte[] rgbKey = Encoding.UTF8.GetBytes(encryptKey[..8]);//转换为字节
+ byte[] rgbIV = Encoding.UTF8.GetBytes(encryptKey[..8]);
+ byte[] inputByteArray = Encoding.UTF8.GetBytes(encryptString);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ var dCSP = new DESCryptoServiceProvider();//实例化数据加密标准
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+ var mStream = new MemoryStream();//实例化内存流
+ //将数据流链接到加密转换的流
+ var cStream = new CryptoStream(mStream, dCSP.CreateEncryptor(rgbKey, rgbIV), CryptoStreamMode.Write);
+ cStream.Write(inputByteArray, 0, inputByteArray.Length);
+ cStream.FlushFinalBlock();
+ // 转base64
+ return Convert.ToBase64String(mStream.ToArray());
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return encryptString;
+ }
+ }
+
+ ///
+ /// DES解密字符串
+ ///
+ /// 待解密的字符串
+ /// 解密密钥,要求为8位,和加密密钥相同
+ /// 解密成功返回解密后的字符串,失败返源串
+ public static string DecryptString(string decryptString, string decryptKey)
+ {
+ try
+ {
+ byte[] rgbKey = Encoding.UTF8.GetBytes(decryptKey);
+ byte[] rgbIV = Encoding.UTF8.GetBytes(decryptKey);
+ byte[] inputByteArray = Convert.FromBase64String(decryptString);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ var DCSP = new DESCryptoServiceProvider();
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+ var mStream = new MemoryStream();
+ var cStream = new CryptoStream(mStream, DCSP.CreateDecryptor(rgbKey, rgbIV), CryptoStreamMode.Write);
+ cStream.Write(inputByteArray, 0, inputByteArray.Length);
+ cStream.FlushFinalBlock();
+ return Encoding.UTF8.GetString(mStream.ToArray());
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return decryptString;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/Encryptor/Hash.cs b/src/Downkyi.Core/Utils/Encryptor/Hash.cs
new file mode 100644
index 0000000..daa7bc4
--- /dev/null
+++ b/src/Downkyi.Core/Utils/Encryptor/Hash.cs
@@ -0,0 +1,67 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Downkyi.Core.Utils.Encryptor;
+
+public static class Hash
+{
+ ///
+ /// 计算字符串MD5值
+ ///
+ ///
+ ///
+ public static string GetMd5Hash(string input)
+ {
+ if (input == null)
+ {
+ return null;
+ }
+
+ MD5 md5Hash = MD5.Create();
+
+ // 将输入字符串转换为字节数组并计算哈希数据
+ byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
+
+ // 创建一个 Stringbuilder 来收集字节并创建字符串
+ var sBuilder = new StringBuilder();
+
+ // 循环遍历哈希数据的每一个字节并格式化为十六进制字符串
+ for (int i = 0; i < data.Length; i++)
+ {
+ sBuilder.Append(data[i].ToString("x2"));
+ }
+
+ // 返回十六进制字符串
+ return sBuilder.ToString();
+ }
+
+ ///
+ /// 计算文件MD5值
+ ///
+ ///
+ ///
+ public static string GetMD5HashFromFile(string fileName)
+ {
+ try
+ {
+ var file = new FileStream(fileName, FileMode.Open);
+#pragma warning disable SYSLIB0021 // 类型或成员已过时
+ MD5 md5 = new MD5CryptoServiceProvider();
+#pragma warning restore SYSLIB0021 // 类型或成员已过时
+ byte[] retVal = md5.ComputeHash(file);
+ file.Close();
+
+ var sb = new StringBuilder();
+ for (int i = 0; i < retVal.Length; i++)
+ {
+ sb.Append(retVal[i].ToString("x2"));
+ }
+ return sb.ToString();
+ }
+ catch (Exception e)
+ {
+ throw new Exception("GetMD5HashFromFile()发生异常: {0}" + e.Message);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/Format.cs b/src/Downkyi.Core/Utils/Format.cs
new file mode 100644
index 0000000..760eb84
--- /dev/null
+++ b/src/Downkyi.Core/Utils/Format.cs
@@ -0,0 +1,220 @@
+using System.Text.RegularExpressions;
+
+namespace Downkyi.Core.Utils;
+
+public static class Format
+{
+
+ ///
+ /// 格式化Duration时间
+ ///
+ ///
+ ///
+ public static string FormatDuration(long duration)
+ {
+ string formatDuration;
+ if (duration / 60 > 0)
+ {
+ long dur = duration / 60;
+ if (dur / 60 > 0)
+ {
+ formatDuration = $"{dur / 60}h{dur % 60}m{duration % 60}s";
+ }
+ else
+ {
+ formatDuration = $"{duration / 60}m{duration % 60}s";
+ }
+ }
+ else
+ {
+ formatDuration = $"{duration}s";
+ }
+ return formatDuration;
+ }
+
+ ///
+ /// 格式化Duration时间,格式为00:00:00
+ ///
+ ///
+ ///
+ public static string FormatDuration2(long duration)
+ {
+ string formatDuration;
+ if (duration / 60 > 0)
+ {
+ long dur = duration / 60;
+ if (dur / 60 > 0)
+ {
+ formatDuration = string.Format("{0:D2}", dur / 60) + ":" + string.Format("{0:D2}", dur % 60) + ":" + string.Format("{0:D2}", duration % 60);
+ }
+ else
+ {
+ formatDuration = "00:" + string.Format("{0:D2}", duration / 60) + ":" + string.Format("{0:D2}", duration % 60);
+ }
+ }
+ else
+ {
+ formatDuration = "00:00:" + string.Format("{0:D2}", duration);
+ }
+ return formatDuration;
+ }
+
+ ///
+ /// 格式化Duration时间,格式为00:00
+ ///
+ ///
+ ///
+ public static string FormatDuration3(long duration)
+ {
+ string formatDuration;
+ if (duration / 60 > 0)
+ {
+ long dur = duration / 60;
+ if (dur / 60 > 0)
+ {
+ formatDuration = string.Format("{0:D2}", dur / 60) + ":" + string.Format("{0:D2}", dur % 60) + ":" + string.Format("{0:D2}", duration % 60);
+ }
+ else
+ {
+ formatDuration = string.Format("{0:D2}", duration / 60) + ":" + string.Format("{0:D2}", duration % 60);
+ }
+ }
+ else
+ {
+ formatDuration = "00:" + string.Format("{0:D2}", duration);
+ }
+ return formatDuration;
+ }
+
+ ///
+ /// 格式化数字,超过10000的数字将单位改为万,超过100000000的数字将单位改为亿,并保留1位小数
+ ///
+ ///
+ ///
+ public static string FormatNumber(long number)
+ {
+ if (number > 99999999)
+ {
+ return (number / 100000000.0f).ToString("F1") + "亿";
+ }
+
+ if (number > 9999)
+ {
+ return (number / 10000.0f).ToString("F1") + "万";
+ }
+ else
+ {
+ return number.ToString();
+ }
+ }
+
+ ///
+ /// 格式化网速
+ ///
+ ///
+ ///
+ public static string FormatSpeed(float speed)
+ {
+ string formatSpeed;
+ if (speed <= 0)
+ {
+ formatSpeed = "0B/s";
+ }
+ else if (speed < 1024)
+ {
+ formatSpeed = string.Format("{0:F2}", speed) + "B/s";
+ }
+ else if (speed < 1024 * 1024)
+ {
+ formatSpeed = string.Format("{0:F2}", speed / 1024) + "KB/s";
+ }
+ else
+ {
+ formatSpeed = string.Format("{0:F2}", speed / 1024 / 1024) + "MB/s";
+ }
+ return formatSpeed;
+ }
+
+ ///
+ /// 格式化字节大小,可用于文件大小的显示
+ ///
+ ///
+ ///
+ public static string FormatFileSize(long fileSize)
+ {
+ string formatFileSize;
+ if (fileSize <= 0)
+ {
+ formatFileSize = "0B";
+ }
+ else if (fileSize < 1024)
+ {
+ formatFileSize = fileSize.ToString() + "B";
+ }
+ else if (fileSize < 1024 * 1024)
+ {
+ formatFileSize = (fileSize / 1024.0).ToString("#.##") + "KB";
+ }
+ else if (fileSize < 1024 * 1024 * 1024)
+ {
+ formatFileSize = (fileSize / 1024.0 / 1024.0).ToString("#.##") + "MB";
+ }
+ else
+ {
+ formatFileSize = (fileSize / 1024.0 / 1024.0 / 1024.0).ToString("#.##") + "GB";
+ }
+ return formatFileSize;
+ }
+
+ ///
+ /// 去除非法字符
+ ///
+ ///
+ ///
+ public static string FormatFileName(string originName)
+ {
+ string destName = originName;
+
+ // Windows中不能作为文件名的字符
+ destName = destName.Replace("\\", " ");
+ destName = destName.Replace("/", " ");
+ destName = destName.Replace(":", " ");
+ destName = destName.Replace("*", " ");
+ destName = destName.Replace("?", " ");
+ destName = destName.Replace("\"", " ");
+ destName = destName.Replace("<", " ");
+ destName = destName.Replace(">", " ");
+ destName = destName.Replace("|", " ");
+
+ // 转义字符
+ destName = destName.Replace("\a", "");
+ destName = destName.Replace("\b", "");
+ destName = destName.Replace("\f", "");
+ destName = destName.Replace("\n", "");
+ destName = destName.Replace("\r", "");
+ destName = destName.Replace("\t", "");
+ destName = destName.Replace("\v", "");
+
+ // 控制字符
+ destName = Regex.Replace(destName, @"\p{C}+", string.Empty);
+
+ // 移除前导和尾部的空白字符、dot符
+ int i, j;
+ for (i = 0; i < destName.Length; i++)
+ {
+ if (destName[i] != ' ' && destName[i] != '.')
+ {
+ break;
+ }
+ }
+ for (j = destName.Length - 1; j >= 0; j--)
+ {
+ if (destName[j] != ' ' && destName[j] != '.')
+ {
+ break;
+ }
+ }
+ return destName.Substring(i, j - i + 1);
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/HardDisk.cs b/src/Downkyi.Core/Utils/HardDisk.cs
new file mode 100644
index 0000000..4b2bba1
--- /dev/null
+++ b/src/Downkyi.Core/Utils/HardDisk.cs
@@ -0,0 +1,64 @@
+namespace Downkyi.Core.Utils;
+
+public static class HardDisk
+{
+ ///
+ /// 获取指定驱动器的空间总大小
+ ///
+ /// 只需输入代表驱动器的字母即可
+ ///
+ public static long GetHardDiskSpace(string hardDiskName)
+ {
+ long totalSize = 0;
+
+ try
+ {
+ hardDiskName = $"{hardDiskName}:\\";
+ DriveInfo[] drives = DriveInfo.GetDrives();
+
+ foreach (DriveInfo drive in drives)
+ {
+ if (drive.Name == hardDiskName)
+ {
+ totalSize = drive.TotalSize;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ }
+
+ return totalSize;
+ }
+
+ ///
+ /// 获取指定驱动器的剩余空间总大小
+ ///
+ /// 只需输入代表驱动器的字母即可
+ ///
+ public static long GetHardDiskFreeSpace(string hardDiskName)
+ {
+ long freeSpace = 0;
+ try
+ {
+ hardDiskName = $"{hardDiskName}:\\";
+ DriveInfo[] drives = DriveInfo.GetDrives();
+
+ foreach (DriveInfo drive in drives)
+ {
+ if (drive.Name == hardDiskName)
+ {
+ freeSpace = drive.TotalFreeSpace;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ }
+
+ return freeSpace;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/LinkStack.cs b/src/Downkyi.Core/Utils/LinkStack.cs
new file mode 100644
index 0000000..a0b9494
--- /dev/null
+++ b/src/Downkyi.Core/Utils/LinkStack.cs
@@ -0,0 +1,87 @@
+namespace Downkyi.Core.Utils;
+
+// 链表实现栈
+public class LinkStack
+{
+ //栈顶指示器
+ public Node Top { get; set; }
+
+ //栈中结点的个数
+ public int NCount { get; set; }
+
+ //初始化
+ public LinkStack()
+ {
+ Top = null;
+ NCount = 0;
+ }
+
+ //获取栈的长度
+ public int GetLength()
+ {
+ return NCount;
+ }
+
+ //判断栈是否为空
+ public bool IsEmpty()
+ {
+ if ((Top == null) && (0 == NCount))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ //入栈
+ public void Push(T item)
+ {
+ Node p = new(item);
+ if (Top == null)
+ {
+ Top = p;
+ }
+ else
+ {
+ p.Next = Top;
+ Top = p;
+ }
+ NCount++;
+ }
+
+ //出栈
+ public T Pop()
+ {
+ if (IsEmpty())
+ {
+ return default;
+ }
+ Node p = Top;
+ Top = Top.Next;
+ --NCount;
+ return p.Data;
+ }
+
+ //
+ public T Peek()
+ {
+ if (IsEmpty())
+ {
+ return default;
+ }
+
+ return Top.Data;
+ }
+}
+
+//结点定义
+public class Node
+{
+ public T Data;
+
+ public Node Next;
+
+ public Node(T item)
+ {
+ Data = item;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/ListHelper.cs b/src/Downkyi.Core/Utils/ListHelper.cs
new file mode 100644
index 0000000..078cd3b
--- /dev/null
+++ b/src/Downkyi.Core/Utils/ListHelper.cs
@@ -0,0 +1,56 @@
+using System.Collections.ObjectModel;
+
+namespace Downkyi.Core.Utils;
+
+public static class ListHelper
+{
+
+ ///
+ /// 判断ObservableCollection中是否存在,不存在则添加
+ ///
+ ///
+ ///
+ ///
+ public static void AddUnique(ObservableCollection list, T item)
+ {
+ if (!list.Contains(item))
+ {
+ list.Add(item);
+ }
+ }
+
+ ///
+ /// 判断List中是否存在,不存在则添加
+ ///
+ ///
+ ///
+ ///
+ public static void AddUnique(List list, T item)
+ {
+ if (!list.Exists(t => t.Equals(item)))
+ {
+ list.Add(item);
+ }
+ }
+
+ ///
+ /// 判断List中是否存在,不存在则添加
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void InsertUnique(List list, T item, int index)
+ {
+ if (!list.Exists(t => t.Equals(item)))
+ {
+ list.Insert(index, item);
+ }
+ else
+ {
+ list.Remove(item);
+ list.Insert(index, item);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/ObjectHelper.cs b/src/Downkyi.Core/Utils/ObjectHelper.cs
new file mode 100644
index 0000000..846d3a4
--- /dev/null
+++ b/src/Downkyi.Core/Utils/ObjectHelper.cs
@@ -0,0 +1,63 @@
+using System.Runtime.Serialization.Formatters.Binary;
+
+namespace Downkyi.Core.Utils;
+
+public static class ObjectHelper
+{
+ ///
+ /// 写入序列化对象到磁盘
+ ///
+ ///
+ ///
+ ///
+ public static bool WriteObjectToDisk(string file, object obj)
+ {
+ try
+ {
+ using Stream stream = File.Create(file);
+ var formatter = new BinaryFormatter();
+#pragma warning disable SYSLIB0011 // 类型或成员已过时
+ formatter.Serialize(stream, obj);
+#pragma warning restore SYSLIB0011 // 类型或成员已过时
+
+ return true;
+ }
+ catch (IOException e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return false;
+ }
+ }
+
+ ///
+ /// 从磁盘读取序列化对象
+ ///
+ ///
+ ///
+ public static object ReadObjectFromDisk(string file)
+ {
+ try
+ {
+ using Stream stream = File.Open(file, FileMode.Open);
+ var formatter = new BinaryFormatter();
+#pragma warning disable SYSLIB0011 // 类型或成员已过时
+ return formatter.Deserialize(stream);
+#pragma warning restore SYSLIB0011 // 类型或成员已过时
+ }
+ catch (IOException e)
+ {
+ Log.Log.Logger.Error(e);
+ return null;
+ }
+ catch (Exception e)
+ {
+ Log.Log.Logger.Error(e);
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.Core/Utils/Stack.cs b/src/Downkyi.Core/Utils/Stack.cs
new file mode 100644
index 0000000..cf133a3
--- /dev/null
+++ b/src/Downkyi.Core/Utils/Stack.cs
@@ -0,0 +1,58 @@
+namespace Downkyi.Core.Utils;
+
+public class Stack
+{
+ private int _maxSize;
+ private int _top = -1;
+ private T[] _data;
+
+ public int Count => _maxSize;
+ public bool IsFull { get => _top == _maxSize - 1; }
+ public bool IsEmpty { get => _top == -1; }
+
+ public Stack(int maxSize)
+ {
+ _maxSize = maxSize;
+ _data = new T[maxSize];
+ }
+
+ public void Push(T value)
+ {
+ if (IsFull)
+ {
+ return;
+ }
+ _data[++_top] = value;
+ }
+
+ public T Pop()
+ {
+ if (IsEmpty)
+ {
+ return default;
+ }
+
+ return _data[_top--];
+ }
+
+ public T Peek()
+ {
+ if (IsEmpty)
+ {
+ return default;
+ }
+
+ return _data[_top];
+ }
+
+ //public Stack? Traverse()
+ //{
+ // if (IsEmpty) { return default; }
+
+ // for (int i = _top; i >= 0; i--)
+ // {
+ // // TODO
+ // }
+ //}
+
+}
diff --git a/src/Downkyi.Core/Utils/Validator/Number.cs b/src/Downkyi.Core/Utils/Validator/Number.cs
new file mode 100644
index 0000000..2d6ef4d
--- /dev/null
+++ b/src/Downkyi.Core/Utils/Validator/Number.cs
@@ -0,0 +1,28 @@
+using System.Text.RegularExpressions;
+
+namespace Downkyi.Core.Utils.Validator;
+
+public static class Number
+{
+
+ ///
+ /// 字符串转数字(长整型)
+ ///
+ ///
+ ///
+ public static long GetInt(string value)
+ {
+ return IsInt(value) ? long.Parse(value) : -1;
+ }
+
+ ///
+ /// 是否为数字
+ ///
+ ///
+ ///
+ public static bool IsInt(string value)
+ {
+ return Regex.IsMatch(value, @"^\d+$");
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Downkyi.UI.csproj b/src/Downkyi.UI/Downkyi.UI.csproj
new file mode 100644
index 0000000..efa17c7
--- /dev/null
+++ b/src/Downkyi.UI/Downkyi.UI.csproj
@@ -0,0 +1,19 @@
+
+
+ net6.0
+ enable
+ enable
+
+
+ none
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi.UI/Models/Cookie.cs b/src/Downkyi.UI/Models/Cookie.cs
new file mode 100644
index 0000000..5b5a4ba
--- /dev/null
+++ b/src/Downkyi.UI/Models/Cookie.cs
@@ -0,0 +1,7 @@
+namespace Downkyi.UI.Models;
+
+public class Cookie
+{
+ public string Key { get; set; } = string.Empty;
+ public string Value { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Models/FileNamePartDisplay.cs b/src/Downkyi.UI/Models/FileNamePartDisplay.cs
new file mode 100644
index 0000000..f67cb86
--- /dev/null
+++ b/src/Downkyi.UI/Models/FileNamePartDisplay.cs
@@ -0,0 +1,16 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Downkyi.Core.FileName;
+
+namespace Downkyi.UI.Models;
+
+public partial class FileNamePartDisplay : ObservableObject
+{
+ [ObservableProperty]
+ public int _index;
+
+ [ObservableProperty]
+ public FileNamePart _id;
+
+ [ObservableProperty]
+ public string _title = string.Empty;
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Models/OrderFormatDisplay.cs b/src/Downkyi.UI/Models/OrderFormatDisplay.cs
new file mode 100644
index 0000000..f9666a5
--- /dev/null
+++ b/src/Downkyi.UI/Models/OrderFormatDisplay.cs
@@ -0,0 +1,9 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.UI.Models;
+
+public class OrderFormatDisplay
+{
+ public OrderFormat Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Models/ParseScopeDisplay.cs b/src/Downkyi.UI/Models/ParseScopeDisplay.cs
new file mode 100644
index 0000000..c18607f
--- /dev/null
+++ b/src/Downkyi.UI/Models/ParseScopeDisplay.cs
@@ -0,0 +1,9 @@
+using Downkyi.Core.Settings.Enum;
+
+namespace Downkyi.UI.Models;
+
+public class ParseScopeDisplay
+{
+ public string Name { get; set; } = string.Empty;
+ public ParseScope ParseScope { get; set; }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Models/TabHeader.cs b/src/Downkyi.UI/Models/TabHeader.cs
new file mode 100644
index 0000000..07a7036
--- /dev/null
+++ b/src/Downkyi.UI/Models/TabHeader.cs
@@ -0,0 +1,9 @@
+namespace Downkyi.UI.Models;
+
+public class TabHeader
+{
+ public long Id { get; set; }
+ public string Title { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public string Icon { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Models/ThirdParty.cs b/src/Downkyi.UI/Models/ThirdParty.cs
new file mode 100644
index 0000000..5479924
--- /dev/null
+++ b/src/Downkyi.UI/Models/ThirdParty.cs
@@ -0,0 +1,40 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using System.Diagnostics;
+
+namespace Downkyi.UI.Models;
+
+public partial class ThirdParty : ObservableObject
+{
+ #region 属性申明
+
+ [ObservableProperty]
+ private string _name = string.Empty;
+
+ [ObservableProperty]
+ private string _author = string.Empty;
+
+ [ObservableProperty]
+ private string _version = string.Empty;
+
+ [ObservableProperty]
+ private string _license = string.Empty;
+
+ [ObservableProperty]
+ private string _homepage = string.Empty;
+
+ [ObservableProperty]
+ private string _licenseUrl = string.Empty;
+
+ #endregion
+
+ #region 命令申明
+
+ [RelayCommand]
+ private void VisitHomepage() { Process.Start(new ProcessStartInfo(Homepage) { UseShellExecute = true }); }
+
+ [RelayCommand]
+ private void GetLicense() { Process.Start(new ProcessStartInfo(LicenseUrl) { UseShellExecute = true }); }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Mvvm/BaseServices.cs b/src/Downkyi.UI/Mvvm/BaseServices.cs
new file mode 100644
index 0000000..58035e3
--- /dev/null
+++ b/src/Downkyi.UI/Mvvm/BaseServices.cs
@@ -0,0 +1,34 @@
+using Downkyi.UI.Services;
+using Downkyi.UI.Services.Event;
+
+namespace Downkyi.UI.Mvvm;
+
+public class BaseServices
+{
+ public IBroadcastEvent BroadcastEvent { get; }
+
+ public INotificationEvent NotificationEvent { get; }
+
+ public IDictionaryResource DictionaryResource { get; }
+
+ public INavigationService NavigationService { get; }
+
+ public IStoragePicker StoragePicker { get; }
+
+ public IStorageService StorageService { get; }
+
+ public BaseServices(IBroadcastEvent broadcastEvent,
+ INotificationEvent notificationEvent,
+ IDictionaryResource dictionaryResource,
+ INavigationService navigationService,
+ IStoragePicker storagePicker,
+ IStorageService storageService)
+ {
+ BroadcastEvent = broadcastEvent;
+ NotificationEvent = notificationEvent;
+ DictionaryResource = dictionaryResource;
+ NavigationService = navigationService;
+ StoragePicker = storagePicker;
+ StorageService = storageService;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Mvvm/Navigation/INavigationAware.cs b/src/Downkyi.UI/Mvvm/Navigation/INavigationAware.cs
new file mode 100644
index 0000000..35e50b6
--- /dev/null
+++ b/src/Downkyi.UI/Mvvm/Navigation/INavigationAware.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.UI.Mvvm.Navigation;
+
+public interface INavigationAware
+{
+ void OnNavigatedTo(Dictionary? parameter);
+
+ void OnNavigatedFrom(Dictionary? parameter);
+}
diff --git a/src/Downkyi.UI/Mvvm/ViewModelBase.cs b/src/Downkyi.UI/Mvvm/ViewModelBase.cs
new file mode 100644
index 0000000..7b76597
--- /dev/null
+++ b/src/Downkyi.UI/Mvvm/ViewModelBase.cs
@@ -0,0 +1,31 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Downkyi.UI.Mvvm.Navigation;
+using Downkyi.UI.Services;
+using Downkyi.UI.Services.Event;
+
+namespace Downkyi.UI.Mvvm;
+
+public abstract class ViewModelBase : ObservableObject, INavigationAware
+{
+ private readonly BaseServices _baseServices;
+
+ protected IBroadcastEvent BroadcastEvent => _baseServices.BroadcastEvent;
+ protected INotificationEvent NotificationEvent => _baseServices.NotificationEvent;
+ protected IDictionaryResource DictionaryResource => _baseServices.DictionaryResource;
+ protected INavigationService NavigationService => _baseServices.NavigationService;
+ protected IStoragePicker StoragePicker => _baseServices.StoragePicker;
+ protected IStorageService StorageService => _baseServices.StorageService;
+
+ public ViewModelBase(BaseServices baseServices)
+ {
+ _baseServices = baseServices;
+ }
+
+ public virtual void OnNavigatedTo(Dictionary? parameter)
+ {
+ }
+
+ public virtual void OnNavigatedFrom(Dictionary? parameter)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/Event/BroadcastEvent.cs b/src/Downkyi.UI/Services/Event/BroadcastEvent.cs
new file mode 100644
index 0000000..8cbbaad
--- /dev/null
+++ b/src/Downkyi.UI/Services/Event/BroadcastEvent.cs
@@ -0,0 +1,23 @@
+namespace Downkyi.UI.Services.Event;
+
+public class BroadcastEvent : IBroadcastEvent
+{
+ private delegate void BroadcastedEventHandler(object obj, string key);
+ private event BroadcastedEventHandler? Broadcasted;
+
+ public void Receive(string key, Action action)
+ {
+ Broadcasted += (obj, k) =>
+ {
+ if (key == k)
+ {
+ Task.Run(() => action?.Invoke(obj));
+ }
+ };
+ }
+
+ public void Send(string key, object obj)
+ {
+ Broadcasted?.Invoke(obj, key);
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/Event/IBroadcastEvent.cs b/src/Downkyi.UI/Services/Event/IBroadcastEvent.cs
new file mode 100644
index 0000000..73ea44c
--- /dev/null
+++ b/src/Downkyi.UI/Services/Event/IBroadcastEvent.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.UI.Services.Event;
+
+public interface IBroadcastEvent
+{
+ void Receive(string key, Action action);
+
+ void Send(string key, object obj);
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/Event/INotificationEvent.cs b/src/Downkyi.UI/Services/Event/INotificationEvent.cs
new file mode 100644
index 0000000..2fc4131
--- /dev/null
+++ b/src/Downkyi.UI/Services/Event/INotificationEvent.cs
@@ -0,0 +1,8 @@
+namespace Downkyi.UI.Services.Event;
+
+public interface INotificationEvent
+{
+ void Subscribe(Action action);
+
+ void Publish(string msg);
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/Event/NotificationEvent.cs b/src/Downkyi.UI/Services/Event/NotificationEvent.cs
new file mode 100644
index 0000000..7362bf0
--- /dev/null
+++ b/src/Downkyi.UI/Services/Event/NotificationEvent.cs
@@ -0,0 +1,18 @@
+namespace Downkyi.UI.Services.Event;
+
+public class NotificationEvent : INotificationEvent
+{
+ private Action? _action;
+ private string _message = string.Empty;
+
+ public void Publish(string msg)
+ {
+ _message = msg;
+ Task.Run(() => _action?.Invoke(_message));
+ }
+
+ public void Subscribe(Action action)
+ {
+ _action = action;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/IDictionaryResource.cs b/src/Downkyi.UI/Services/IDictionaryResource.cs
new file mode 100644
index 0000000..da9b461
--- /dev/null
+++ b/src/Downkyi.UI/Services/IDictionaryResource.cs
@@ -0,0 +1,30 @@
+namespace Downkyi.UI.Services;
+
+public interface IDictionaryResource
+{
+ ///
+ /// 从资源获取颜色的16进制字符串
+ ///
+ ///
+ ///
+ string GetColor(string resourceKey);
+
+ ///
+ /// 从资源获取字符串
+ ///
+ ///
+ ///
+ string GetString(string resourceKey);
+
+ ///
+ /// 根据languageCode切换界面语言
+ ///
+ ///
+ void LoadLanguage(string languageCode);
+
+ ///
+ /// 切换主题
+ ///
+ ///
+ void LoadTheme(string theme);
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/INavigationService.cs b/src/Downkyi.UI/Services/INavigationService.cs
new file mode 100644
index 0000000..5f44a07
--- /dev/null
+++ b/src/Downkyi.UI/Services/INavigationService.cs
@@ -0,0 +1,12 @@
+namespace Downkyi.UI.Services;
+
+public interface INavigationService
+{
+ Task ForwardAsync(string viewKey);
+
+ Task ForwardAsync(string viewKey, Dictionary parameter);
+
+ Task BackwardAsync();
+
+ Task BackwardAsync(Dictionary parameter);
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/IStoragePicker.cs b/src/Downkyi.UI/Services/IStoragePicker.cs
new file mode 100644
index 0000000..620ef8f
--- /dev/null
+++ b/src/Downkyi.UI/Services/IStoragePicker.cs
@@ -0,0 +1,23 @@
+namespace Downkyi.UI.Services;
+
+public interface IStoragePicker
+{
+ ///
+ /// 弹出选择文件夹弹窗
+ ///
+ ///
+ ///
+ Task FolderPicker(string directory = "");
+
+ ///
+ /// 选择视频dialog
+ ///
+ ///
+ Task SelectVideoFileAsync();
+
+ ///
+ /// 选择多个视频dialog
+ ///
+ ///
+ Task> SelectMultiVideoFileAsync();
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/Services/IStorageService.cs b/src/Downkyi.UI/Services/IStorageService.cs
new file mode 100644
index 0000000..3ee4904
--- /dev/null
+++ b/src/Downkyi.UI/Services/IStorageService.cs
@@ -0,0 +1,5 @@
+namespace Downkyi.UI.Services;
+
+public interface IStorageService
+{
+}
diff --git a/src/Downkyi.UI/Services/StorageService.cs b/src/Downkyi.UI/Services/StorageService.cs
new file mode 100644
index 0000000..ecedc3b
--- /dev/null
+++ b/src/Downkyi.UI/Services/StorageService.cs
@@ -0,0 +1,5 @@
+namespace Downkyi.UI.Services;
+
+public class StorageService : IStorageService
+{
+}
diff --git a/src/Downkyi.UI/ViewModels/DownloadManager/DownloadFinishedViewModel.cs b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadFinishedViewModel.cs
new file mode 100644
index 0000000..ee62d74
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadFinishedViewModel.cs
@@ -0,0 +1,12 @@
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.DownloadManager;
+
+public class DownloadFinishedViewModel : ViewModelBase
+{
+ public const string Key = "DownloadManager_DownloadFinished";
+
+ public DownloadFinishedViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/DownloadManager/DownloadManagerViewModel.cs b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadManagerViewModel.cs
new file mode 100644
index 0000000..6cb0e21
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadManagerViewModel.cs
@@ -0,0 +1,47 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Models;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.DownloadManager;
+
+public partial class DownloadManagerViewModel : ViewModelBase
+{
+ public const string Key = "DownloadManager";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private int _selectedTabId = -1;
+
+ [ObservableProperty]
+ private List _tabHeaders = new();
+
+ #endregion
+
+ public DownloadManagerViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+
+ TabHeaders.Add(new TabHeader { Id = 0, Title = DictionaryResource.GetString("Downloading") });
+ TabHeaders.Add(new TabHeader { Id = 1, Title = DictionaryResource.GetString("DownloadFinished") });
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task BackwardAsync()
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ };
+
+ await NavigationService.BackwardAsync(parameter);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/DownloadManager/DownloadingViewModel.cs b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadingViewModel.cs
new file mode 100644
index 0000000..662d2f2
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadingViewModel.cs
@@ -0,0 +1,12 @@
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.DownloadManager;
+
+public class DownloadingViewModel : ViewModelBase
+{
+ public const string Key = "DownloadManager_Downloading";
+
+ public DownloadingViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/IndexViewModel.cs b/src/Downkyi.UI/ViewModels/IndexViewModel.cs
new file mode 100644
index 0000000..c18747d
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/IndexViewModel.cs
@@ -0,0 +1,188 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Bili;
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Models;
+using Downkyi.Core.Storage;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.DownloadManager;
+using Downkyi.UI.ViewModels.Login;
+using Downkyi.UI.ViewModels.Settings;
+using Downkyi.UI.ViewModels.Toolbox;
+using Downkyi.UI.ViewModels.User;
+
+namespace Downkyi.UI.ViewModels;
+
+public partial class IndexViewModel : ViewModelBase
+{
+ public const string Key = "Index";
+
+ private const string defaultHeader = "avares://Downkyi/Assets/bili/default_header.jpg";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private bool _loginPanelVisibility = false;
+
+ [ObservableProperty]
+ private string _header = defaultHeader;
+
+ [ObservableProperty]
+ private string? _userName = null;
+
+ [ObservableProperty]
+ private string _inputText = string.Empty;
+
+ #endregion
+
+ public IndexViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+ #endregion
+ }
+
+ ///
+ /// 导航到页面时执行
+ ///
+ ///
+ public override async void OnNavigatedTo(Dictionary? parameter)
+ {
+ base.OnNavigatedTo(parameter);
+
+ await UpdateUserInfo();
+ }
+
+ #region 命令申明
+
+ ///
+ /// 进入登录页面
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task LoginAsync()
+ {
+ if (UserName == null || UserName == string.Empty)
+ {
+ await NavigationService.ForwardAsync(LoginViewModel.Key);
+ }
+ else
+ {
+ // 进入用户空间
+ var userInfo = SettingsManager.GetInstance().GetUserInfo();
+ if (userInfo != null && userInfo.Mid != -1)
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ { "mid", userInfo.Mid },
+ };
+ await NavigationService.ForwardAsync(MySpaceViewModel.Key, parameter);
+ }
+ }
+ }
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private Task InputAsync()
+ {
+ EnterBili();
+
+ return Task.CompletedTask;
+ }
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task SettingsAsync()
+ {
+ await NavigationService.ForwardAsync(SettingsViewModel.Key);
+ }
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task DownloadManagerAsync()
+ {
+ await NavigationService.ForwardAsync(DownloadManagerViewModel.Key);
+ }
+
+ ///
+ /// 进入工具箱页面
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task ToolboxAsync()
+ {
+ await NavigationService.ForwardAsync(ToolboxViewModel.Key);
+ }
+
+ #endregion
+
+ #region 业务逻辑
+
+ private void EnterBili()
+ {
+ NotificationEvent.Publish("Enter Bili!");
+ }
+
+ private async Task UpdateUserInfo()
+ {
+ LoginPanelVisibility = false;
+
+ // 检查本地是否存在login文件,没有则说明未登录
+ if (!File.Exists(StorageManager.GetLogin()))
+ {
+ LoginPanelVisibility = true;
+ Header = defaultHeader;
+ UserName = null;
+ return;
+ }
+
+ await Task.Run(() =>
+ {
+ // 获取用户信息
+ var userInfo = BiliLocator.Login.GetNavigationInfo();
+ if (userInfo != null)
+ {
+ SettingsManager.GetInstance().SetUserInfo(new UserInfoSettings
+ {
+ Mid = userInfo.Mid,
+ Name = userInfo.Name,
+ IsLogin = userInfo.IsLogin,
+ IsVip = userInfo.VipStatus == 1
+ });
+ }
+ else
+ {
+ SettingsManager.GetInstance().SetUserInfo(new UserInfoSettings
+ {
+ Mid = -1,
+ Name = "",
+ IsLogin = false,
+ IsVip = false
+ });
+ }
+
+ //
+ LoginPanelVisibility = true;
+
+ // 设置头像和用户名
+ if (userInfo != null)
+ {
+ if (userInfo.Header != null)
+ {
+ Header = userInfo.Header;
+ }
+ else
+ {
+ Header = defaultHeader;
+ }
+ UserName = userInfo.Name;
+ }
+ else
+ {
+ Header = defaultHeader;
+ UserName = null;
+ }
+ });
+
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Login/CookiesViewModel.cs b/src/Downkyi.UI/ViewModels/Login/CookiesViewModel.cs
new file mode 100644
index 0000000..0e41bdf
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Login/CookiesViewModel.cs
@@ -0,0 +1,104 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Bili.Web;
+using Downkyi.Core.Log;
+using Downkyi.UI.Mvvm;
+using System.Collections.ObjectModel;
+using System.Net;
+
+namespace Downkyi.UI.ViewModels.Login;
+
+public partial class CookiesViewModel : ViewModelBase
+{
+ public const string Key = "Login_Cookies";
+
+ private CookieContainer? _cookieContainer;
+
+ private bool _isParse;
+
+ protected string LoginSuccessful = string.Empty;
+
+ protected string LoginFailed = string.Empty;
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private string _cookiesStr = string.Empty;
+
+ [ObservableProperty]
+ private ObservableCollection _cookies = new();
+
+ #endregion
+
+ public CookiesViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ LoginSuccessful = DictionaryResource.GetString("CookiesLoginSuccessful");
+ LoginFailed = DictionaryResource.GetString("CookiesLoginFailed");
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private Task TextChangedAsync()
+ {
+ if (_isParse) { return Task.CompletedTask; }
+
+ _isParse = true;
+
+ try
+ {
+ _cookieContainer = Core.Bili.Cookies.ParseCookieByString(CookiesStr);
+ var cookies = Core.Bili.Cookies.GetAllCookies(_cookieContainer);
+
+ Cookies.Clear();
+ foreach (var cookie in cookies)
+ {
+ Cookies.Add(new Models.Cookie { Key = cookie.Name, Value = cookie.Value });
+ }
+ }
+ catch (Exception) { }
+
+ _isParse = false;
+ return Task.CompletedTask;
+ }
+
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private Task SaveCookiesAsync()
+ {
+ if (_cookieContainer == null)
+ {
+ return Task.CompletedTask;
+ }
+
+ // 保存登录信息
+ try
+ {
+ bool isSucceed = LoginHelper.SaveLoginInfoCookies(_cookieContainer);
+ if (!isSucceed)
+ {
+ Log.Logger.Error($"{Key}: {LoginFailed}");
+ NotificationEvent.Publish(LoginFailed);
+
+ return Task.CompletedTask;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Logger.Error(e);
+ NotificationEvent.Publish(LoginFailed);
+
+ return Task.CompletedTask;
+ }
+
+ // 发送通知
+ NotificationEvent.Publish(LoginSuccessful);
+ Log.Logger.Info($"{Key}: {LoginSuccessful}");
+
+ BroadcastEvent.Send("cookiesLogin", "loginSuccessful");
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Login/LoginViewModel.cs b/src/Downkyi.UI/ViewModels/Login/LoginViewModel.cs
new file mode 100644
index 0000000..b94a647
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Login/LoginViewModel.cs
@@ -0,0 +1,70 @@
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Login;
+
+public partial class LoginViewModel : ViewModelBase
+{
+ public const string Key = "Login";
+
+ private bool _isBackward = false;
+
+ private bool _result = false;
+
+ public LoginViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ BroadcastEvent.Receive("qrcodeLogin",
+ new Action((obj) =>
+ {
+ if (_isBackward) { return; }
+
+ string? value = obj as string;
+ if (value == "loginSuccessful")
+ {
+ _result = true;
+ _ = BackwardAsync();
+ }
+ if (value == "getLoginUrlFailed")
+ {
+ _result = false;
+ _ = BackwardAsync();
+ }
+ })
+ );
+
+ BroadcastEvent.Receive("cookiesLogin",
+ new Action((obj) =>
+ {
+ if (_isBackward) { return; }
+
+ string? value = obj as string;
+ if (value == "loginSuccessful")
+ {
+ _result = true;
+ _ = BackwardAsync();
+ }
+ })
+ );
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task BackwardAsync()
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ { "result", _result },
+ };
+
+ await NavigationService.BackwardAsync(parameter);
+
+ BroadcastEvent.Send("login", "backward");
+
+ _isBackward = true;
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Login/QRCodeViewModel.cs b/src/Downkyi.UI/ViewModels/Login/QRCodeViewModel.cs
new file mode 100644
index 0000000..30fbe48
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Login/QRCodeViewModel.cs
@@ -0,0 +1,197 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Bili;
+using Downkyi.Core.Bili.Web;
+using Downkyi.Core.Log;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Login;
+
+public partial class QRCodeViewModel : ViewModelBase
+{
+ public const string Key = "Login_QRCode";
+
+ private CancellationTokenSource? tokenSource;
+
+ protected Action? _QRCodeAction;
+
+ protected Action? _loginAction;
+
+ protected string GetLoginUrlFailed = string.Empty;
+
+ protected string LoginKeyError = string.Empty;
+
+ protected string LoginTimeOut = string.Empty;
+
+ protected string LoginSuccessful = string.Empty;
+
+ protected string LoginFailed = string.Empty;
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private double _QRCodeOpacity;
+
+ [ObservableProperty]
+ private bool _QRCodeStatus;
+
+ #endregion
+
+ public QRCodeViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ GetLoginUrlFailed = DictionaryResource.GetString("GetLoginUrlFailed");
+ LoginKeyError = DictionaryResource.GetString("LoginKeyError");
+ LoginTimeOut = DictionaryResource.GetString("LoginTimeOut");
+ LoginSuccessful = DictionaryResource.GetString("LoginSuccessful");
+ LoginFailed = DictionaryResource.GetString("LoginFailed");
+ }
+
+ #region 命令申明
+
+ [RelayCommand]
+ private async Task OnLoaded()
+ {
+ // 初始化状态
+ InitStatus();
+
+ await Task.Run(Login, (tokenSource = new CancellationTokenSource()).Token);
+ }
+
+ [RelayCommand]
+ private void OnUnloaded()
+ {
+ // 初始化状态
+ InitStatus();
+
+ // 结束任务
+ tokenSource?.Cancel();
+ }
+
+ #endregion
+
+ #region 业务逻辑
+
+ ///
+ /// 登录
+ ///
+ private async Task Login()
+ {
+ try
+ {
+ var qrcode = BiliLocator.Login.GetQRCodeUrl();
+ if (qrcode == null)
+ {
+ OnUnloaded();
+ BroadcastEvent.Send("qrcodeLogin", "getLoginUrlFailed");
+ NotificationEvent.Publish(GetLoginUrlFailed);
+ return;
+ }
+
+ // 传递给主项目生成二维码
+ _QRCodeAction?.Invoke(qrcode.Item1);
+
+ Log.Logger.Debug($"{Key}: {qrcode.Item1}");
+
+ // poll
+ await GetLoginStatus(qrcode.Item2);
+ }
+ catch (Exception e)
+ {
+ Log.Logger.Error(e);
+ }
+ }
+
+ ///
+ /// 循环查询登录状态
+ ///
+ ///
+ private async Task GetLoginStatus(string oauthKey)
+ {
+ CancellationToken cancellationToken = tokenSource!.Token;
+ while (true)
+ {
+ await Task.Delay(1000);
+ var loginStatus = BiliLocator.Login.PollQRCode(oauthKey);
+ if (loginStatus == null) { continue; }
+
+ switch (loginStatus.Code)
+ {
+ case 0: // 扫码登录成功
+
+ // 发送通知
+ NotificationEvent.Publish(LoginSuccessful);
+ Log.Logger.Info($"{Key}: {LoginSuccessful}");
+
+ // 保存登录信息
+ try
+ {
+ bool isSucceed = LoginHelper.SaveLoginInfoCookies(loginStatus.Url);
+ if (!isSucceed)
+ {
+ NotificationEvent.Publish(LoginFailed);
+ Log.Logger.Error($"{Key}: {LoginFailed}");
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Logger.Error(e);
+ NotificationEvent.Publish(LoginFailed);
+ }
+
+ // 取消任务
+ await Task.Delay(3000);
+
+ OnUnloaded();
+ BroadcastEvent.Send("qrcodeLogin", "loginSuccessful");
+
+ break;
+ case 86038: // 二维码已失效
+ // 发送通知
+ NotificationEvent.Publish(LoginTimeOut);
+ Log.Logger.Info($"{Key}: {LoginTimeOut}");
+ // 取消任务
+ tokenSource.Cancel();
+ // 创建新任务
+ await Task.Run(Login, (tokenSource = new CancellationTokenSource()).Token);
+ break;
+ case 86090: // 二维码已扫码未确认
+ QRCodeOpacity = 0.1;
+ QRCodeStatus = true;
+ break;
+ case 86101: // 未扫码
+ break;
+ }
+
+ BroadcastEvent.Receive("login",
+ new Action((obj) =>
+ {
+ string? v = obj as string;
+ if (v == "backward")
+ {
+ // 取消任务
+ tokenSource.Cancel();
+ }
+ })
+ );
+
+ // 判断是否该结束线程,若为true,跳出while循环
+ if (cancellationToken.IsCancellationRequested)
+ {
+ Log.Logger.Debug($"{Key}: 登录操作结束");
+ break;
+ }
+ }
+ }
+
+ ///
+ /// 初始化状态
+ ///
+ protected virtual void InitStatus()
+ {
+ QRCodeOpacity = 1;
+ QRCodeStatus = false;
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/AboutViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/AboutViewModel.cs
new file mode 100644
index 0000000..309e455
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/AboutViewModel.cs
@@ -0,0 +1,115 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Enum;
+using Downkyi.UI.Models;
+using Downkyi.UI.Mvvm;
+using System.Diagnostics;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public partial class AboutViewModel : BaseSettingsViewModel
+{
+ public const string Key = "Settings_About";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private string _appName = string.Empty;
+
+ [ObservableProperty]
+ private string _appVersion = string.Empty;
+
+ [ObservableProperty]
+ private bool _isReceiveBetaVersion;
+
+ [ObservableProperty]
+ private bool _isAutoUpdateWhenLaunch;
+
+ [ObservableProperty]
+ private List _thirdParties = new();
+
+ #endregion
+
+ public AboutViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated");
+ TipSettingFailed = DictionaryResource.GetString("TipSettingFailed");
+ }
+
+ #region 命令申明
+
+ ///
+ /// 加载页面时执行
+ ///
+ [RelayCommand]
+ private void OnLoaded()
+ {
+ IsOnNavigatedTo = true;
+
+ // 是否接收测试版更新
+ var isReceiveBetaVersion = SettingsManager.GetInstance().IsReceiveBetaVersion();
+ IsReceiveBetaVersion = isReceiveBetaVersion == AllowStatus.YES;
+
+ // 是否在启动时自动检查更新
+ var isAutoUpdateWhenLaunch = SettingsManager.GetInstance().GetAutoUpdateWhenLaunch();
+ IsAutoUpdateWhenLaunch = isAutoUpdateWhenLaunch == AllowStatus.YES;
+
+ IsOnNavigatedTo = false;
+ }
+
+ ///
+ /// 访问主页事件
+ ///
+ [RelayCommand]
+ private void VisitHomepage()
+ {
+ Process.Start(new ProcessStartInfo("https://github.com/leiurayer/downkyi") { UseShellExecute = true });
+ }
+
+ ///
+ /// 检查更新事件
+ ///
+ [RelayCommand]
+ private void CheckUpdate()
+ {
+ // TODO
+ NotificationEvent.Publish("请前往主页下载最新版~");
+ }
+
+ ///
+ /// 意见反馈事件
+ ///
+ [RelayCommand]
+ private void Feedback()
+ {
+ Process.Start(new ProcessStartInfo("https://github.com/leiurayer/downkyi/issues") { UseShellExecute = true });
+ }
+
+ ///
+ /// 是否接收测试版更新事件
+ ///
+ [RelayCommand]
+ private void ReceiveBetaVersion()
+ {
+ AllowStatus isReceiveBetaVersion = IsReceiveBetaVersion ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsReceiveBetaVersion(isReceiveBetaVersion);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否在启动时自动检查更新事件
+ ///
+ [RelayCommand]
+ private void AutoUpdateWhenLaunch()
+ {
+ AllowStatus isAutoUpdateWhenLaunch = IsAutoUpdateWhenLaunch ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().SetAutoUpdateWhenLaunch(isAutoUpdateWhenLaunch);
+ PublishTip(Key, isSucceed);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/BaseSettingsViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/BaseSettingsViewModel.cs
new file mode 100644
index 0000000..7caea0d
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/BaseSettingsViewModel.cs
@@ -0,0 +1,43 @@
+using Downkyi.Core.Log;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public class BaseSettingsViewModel : ViewModelBase
+{
+ protected bool IsOnNavigatedTo;
+
+ protected string TipSettingUpdated = string.Empty;
+
+ protected string TipSettingFailed = string.Empty;
+
+ public BaseSettingsViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ }
+
+ #region UI逻辑
+
+ ///
+ /// 发送需要显示的tip
+ ///
+ ///
+ protected void PublishTip(string key, bool isSucceed)
+ {
+ if (IsOnNavigatedTo) { return; }
+
+ // 发送通知
+ if (isSucceed)
+ {
+ NotificationEvent.Publish(TipSettingUpdated);
+ Log.Logger.Info($"{key}: {TipSettingUpdated}");
+ }
+ else
+ {
+ NotificationEvent.Publish(TipSettingFailed);
+ Log.Logger.Info($"{key}: {TipSettingFailed}");
+ }
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/BasicViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/BasicViewModel.cs
new file mode 100644
index 0000000..05646ef
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/BasicViewModel.cs
@@ -0,0 +1,189 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Enum;
+using Downkyi.UI.Models;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public partial class BasicViewModel : BaseSettingsViewModel
+{
+ public const string Key = "Settings_Basic";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private bool _none;
+
+ [ObservableProperty]
+ private bool _closeApp;
+
+ [ObservableProperty]
+ private bool _closeSystem;
+
+ [ObservableProperty]
+ private bool _isListenClipboard;
+
+ [ObservableProperty]
+ private bool _isAutoParseVideo;
+
+ [ObservableProperty]
+ private List _parseScopes = new();
+
+ [ObservableProperty]
+ private ParseScopeDisplay? _selectedParseScope;
+
+ [ObservableProperty]
+ private bool _isAutoDownloadAll;
+
+ #endregion
+
+ public BasicViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated");
+ TipSettingFailed = DictionaryResource.GetString("TipSettingFailed");
+
+ #region 属性初始化
+
+ // 解析范围
+ ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseNone"), ParseScope = ParseScope.NONE });
+ ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseSelectedItem"), ParseScope = ParseScope.SELECTED_ITEM });
+ ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseCurrentSection"), ParseScope = ParseScope.CURRENT_SECTION });
+ ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseAll"), ParseScope = ParseScope.ALL });
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ ///
+ /// 加载页面时执行
+ ///
+ [RelayCommand]
+ private void OnLoaded()
+ {
+ IsOnNavigatedTo = true;
+
+ // 下载完成后的操作
+ AfterDownloadOperation afterDownload = SettingsManager.GetInstance().GetAfterDownloadOperation();
+ SetAfterDownloadOperation(afterDownload);
+
+ // 是否监听剪贴板
+ AllowStatus isListenClipboard = SettingsManager.GetInstance().IsListenClipboard();
+ IsListenClipboard = isListenClipboard == AllowStatus.YES;
+
+ // 是否自动解析视频
+ AllowStatus isAutoParseVideo = SettingsManager.GetInstance().IsAutoParseVideo();
+ IsAutoParseVideo = isAutoParseVideo == AllowStatus.YES;
+
+ // 解析范围
+ ParseScope parseScope = SettingsManager.GetInstance().GetParseScope();
+ SelectedParseScope = ParseScopes.FirstOrDefault(t => { return t.ParseScope == parseScope; });
+
+ // 解析后是否自动下载解析视频
+ AllowStatus isAutoDownloadAll = SettingsManager.GetInstance().IsAutoDownloadAll();
+ IsAutoDownloadAll = isAutoDownloadAll == AllowStatus.YES;
+
+ IsOnNavigatedTo = false;
+ }
+
+ ///
+ /// 下载完成后的操作事件
+ ///
+ ///
+ [RelayCommand]
+ private void AfterDownload(object parameter)
+ {
+ if (parameter is not string) { return; }
+
+ var afterDownload = parameter switch
+ {
+ "None" => AfterDownloadOperation.NONE,
+ "CloseApp" => AfterDownloadOperation.CLOSE_APP,
+ "CloseSystem" => AfterDownloadOperation.CLOSE_SYSTEM,
+ _ => AfterDownloadOperation.NONE,
+ };
+ bool isSucceed = SettingsManager.GetInstance().SetAfterDownloadOperation(afterDownload);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否监听剪贴板事件
+ ///
+ [RelayCommand]
+ private void ListenClipboard()
+ {
+ AllowStatus isListenClipboard = IsListenClipboard ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsListenClipboard(isListenClipboard);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否自动解析视频
+ ///
+ [RelayCommand]
+ private void AutoParseVideo()
+ {
+ AllowStatus isAutoParseVideo = IsAutoParseVideo ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsAutoParseVideo(isAutoParseVideo);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 解析范围事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetParseScopes()
+ {
+ //if (parameter is not ParseScopeDisplay parseScope) { return; }
+
+ bool isSucceed = SettingsManager.GetInstance().SetParseScope(SelectedParseScope!.ParseScope);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 解析后是否自动下载解析视频
+ ///
+ [RelayCommand]
+ private void AutoDownloadAll()
+ {
+ AllowStatus isAutoDownloadAll = IsAutoDownloadAll ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsAutoDownloadAll(isAutoDownloadAll);
+ PublishTip(Key, isSucceed);
+ }
+
+ #endregion
+
+
+ #region UI逻辑
+
+ ///
+ /// 设置下载完成后的操作
+ ///
+ ///
+ private void SetAfterDownloadOperation(AfterDownloadOperation afterDownload)
+ {
+ switch (afterDownload)
+ {
+ case AfterDownloadOperation.NONE:
+ None = true;
+ break;
+ case AfterDownloadOperation.OPEN_FOLDER:
+ break;
+ case AfterDownloadOperation.CLOSE_APP:
+ CloseApp = true;
+ break;
+ case AfterDownloadOperation.CLOSE_SYSTEM:
+ CloseSystem = true;
+ break;
+ }
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/DanmakuViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/DanmakuViewModel.cs
new file mode 100644
index 0000000..7ddd10b
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/DanmakuViewModel.cs
@@ -0,0 +1,265 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Enum;
+using Downkyi.Core.Utils.Validator;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public partial class DanmakuViewModel : BaseSettingsViewModel
+{
+ public const string Key = "Settings_Danmaku";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private bool _topFilter;
+
+ [ObservableProperty]
+ private bool _bottomFilter;
+
+ [ObservableProperty]
+ private bool _scrollFilter;
+
+ [ObservableProperty]
+ private int _screenWidth;
+
+ [ObservableProperty]
+ private int _screenHeight;
+
+ [ObservableProperty]
+ private List _fonts = new();
+
+ [ObservableProperty]
+ private string _selectedFont = string.Empty;
+
+ [ObservableProperty]
+ private int _fontSize;
+
+ [ObservableProperty]
+ private int _lineCount;
+
+ [ObservableProperty]
+ private bool _sync;
+
+ [ObservableProperty]
+ private bool _async;
+
+ #endregion
+
+ public DanmakuViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated");
+ TipSettingFailed = DictionaryResource.GetString("TipSettingFailed");
+ }
+
+ #region 命令申明
+
+ ///
+ /// 加载页面时执行
+ ///
+ [RelayCommand]
+ private void OnLoaded()
+ {
+ IsOnNavigatedTo = true;
+
+ // 屏蔽顶部弹幕
+ AllowStatus danmakuTopFilter = SettingsManager.GetInstance().GetDanmakuTopFilter();
+ TopFilter = danmakuTopFilter == AllowStatus.YES;
+
+ // 屏蔽底部弹幕
+ AllowStatus danmakuBottomFilter = SettingsManager.GetInstance().GetDanmakuBottomFilter();
+ BottomFilter = danmakuBottomFilter == AllowStatus.YES;
+
+ // 屏蔽滚动弹幕
+ AllowStatus danmakuScrollFilter = SettingsManager.GetInstance().GetDanmakuScrollFilter();
+ ScrollFilter = danmakuScrollFilter == AllowStatus.YES;
+
+ // 分辨率-宽
+ ScreenWidth = SettingsManager.GetInstance().GetDanmakuScreenWidth();
+
+ // 分辨率-高
+ ScreenHeight = SettingsManager.GetInstance().GetDanmakuScreenHeight();
+
+ // 弹幕字体
+ string danmakuFont = SettingsManager.GetInstance().GetDanmakuFontName();
+ if (danmakuFont != null && Fonts.Contains(danmakuFont))
+ {
+ // 只有系统中存在当前设置的字体,才能显示
+ SelectedFont = danmakuFont;
+ }
+
+ // 弹幕字体大小
+ FontSize = SettingsManager.GetInstance().GetDanmakuFontSize();
+
+ // 弹幕限制行数
+ LineCount = SettingsManager.GetInstance().GetDanmakuLineCount();
+
+ // 弹幕布局算法
+ DanmakuLayoutAlgorithm layoutAlgorithm = SettingsManager.GetInstance().GetDanmakuLayoutAlgorithm();
+ SetLayoutAlgorithm(layoutAlgorithm);
+
+ IsOnNavigatedTo = false;
+ }
+
+ ///
+ /// 屏蔽顶部弹幕事件
+ ///
+ [RelayCommand]
+ private void SetTopFilter()
+ {
+ AllowStatus isTopFilter = TopFilter ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuTopFilter(isTopFilter);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 屏蔽底部弹幕事件
+ ///
+ [RelayCommand]
+ private void SetBottomFilter()
+ {
+ AllowStatus isBottomFilter = BottomFilter ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuBottomFilter(isBottomFilter);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 屏蔽滚动弹幕事件
+ ///
+ [RelayCommand]
+ private void SetScrollFilter()
+ {
+ AllowStatus isScrollFilter = ScrollFilter ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuScrollFilter(isScrollFilter);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 设置分辨率-宽事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetScreenWidth(string parameter)
+ {
+ int width = (int)Number.GetInt(parameter);
+ ScreenWidth = width;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuScreenWidth(ScreenWidth);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 设置分辨率-高事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetScreenHeight(string parameter)
+ {
+ int height = (int)Number.GetInt(parameter);
+ ScreenHeight = height;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuScreenHeight(ScreenHeight);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 弹幕字体选择事件
+ ///
+ ///
+ [RelayCommand]
+ private void FontSelected(string parameter)
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuFontName(parameter);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 弹幕字体大小事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetFontSize(string parameter)
+ {
+ int fontSize = (int)Number.GetInt(parameter);
+ FontSize = fontSize;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuFontSize(FontSize);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 弹幕限制行数事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetLineCount(string parameter)
+ {
+ int lineCount = (int)Number.GetInt(parameter);
+ LineCount = lineCount;
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuLineCount(LineCount);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 弹幕布局算法事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetLayoutAlgorithm(string parameter)
+ {
+ DanmakuLayoutAlgorithm layoutAlgorithm;
+ switch (parameter)
+ {
+ case "Sync":
+ layoutAlgorithm = DanmakuLayoutAlgorithm.SYNC;
+ break;
+ case "Async":
+ layoutAlgorithm = DanmakuLayoutAlgorithm.ASYNC;
+ break;
+ default:
+ layoutAlgorithm = DanmakuLayoutAlgorithm.SYNC;
+ break;
+ }
+
+ bool isSucceed = SettingsManager.GetInstance().SetDanmakuLayoutAlgorithm(layoutAlgorithm);
+ PublishTip(Key, isSucceed);
+
+ if (isSucceed)
+ {
+ SetLayoutAlgorithm(layoutAlgorithm);
+ }
+ }
+
+ #endregion
+
+ ///
+ /// 设置弹幕同步算法
+ ///
+ ///
+ private void SetLayoutAlgorithm(DanmakuLayoutAlgorithm layoutAlgorithm)
+ {
+ switch (layoutAlgorithm)
+ {
+ case DanmakuLayoutAlgorithm.SYNC:
+ Sync = true;
+ Async = false;
+ break;
+ case DanmakuLayoutAlgorithm.ASYNC:
+ Sync = false;
+ Async = true;
+ break;
+ case DanmakuLayoutAlgorithm.NONE:
+ Sync = false;
+ Async = false;
+ break;
+ default:
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/NetworkViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/NetworkViewModel.cs
new file mode 100644
index 0000000..acd18b1
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/NetworkViewModel.cs
@@ -0,0 +1,518 @@
+using Aria2cNet.Server;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Enum;
+using Downkyi.Core.Utils.Validator;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public partial class NetworkViewModel : BaseSettingsViewModel
+{
+ public const string Key = "Settings_Network";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private bool _useSSL;
+
+ [ObservableProperty]
+ private string _userAgent = string.Empty;
+
+ [ObservableProperty]
+ private bool _builtin;
+
+ [ObservableProperty]
+ private bool _aria2c;
+
+ [ObservableProperty]
+ private bool _customAria2c;
+
+ [ObservableProperty]
+ private List _maxCurrentDownloads = new();
+
+ [ObservableProperty]
+ private int _selectedMaxCurrentDownload;
+
+ [ObservableProperty]
+ private List _splits = new();
+
+ [ObservableProperty]
+ private int _selectedSplit;
+
+ [ObservableProperty]
+ private bool _isHttpProxy;
+
+ [ObservableProperty]
+ private string _httpProxy = string.Empty;
+
+ [ObservableProperty]
+ private int _httpProxyPort;
+
+ [ObservableProperty]
+ private string _ariaHost = string.Empty;
+
+ [ObservableProperty]
+ private int _ariaListenPort;
+
+ [ObservableProperty]
+ private string _ariaToken = string.Empty;
+
+ [ObservableProperty]
+ private List _ariaLogLevels = new();
+
+ [ObservableProperty]
+ private string _selectedAriaLogLevel = string.Empty;
+
+ [ObservableProperty]
+ private List _ariaMaxConcurrentDownloads = new();
+
+ [ObservableProperty]
+ private int _selectedAriaMaxConcurrentDownload;
+
+ [ObservableProperty]
+ private List _ariaSplits = new();
+
+ [ObservableProperty]
+ private int _selectedAriaSplit;
+
+ [ObservableProperty]
+ private int _ariaMaxOverallDownloadLimit;
+
+ [ObservableProperty]
+ private int _ariaMaxDownloadLimit;
+
+ [ObservableProperty]
+ private bool _isAriaHttpProxy;
+
+ [ObservableProperty]
+ private string _ariaHttpProxy = string.Empty;
+
+ [ObservableProperty]
+ private int _ariaHttpProxyPort;
+
+ [ObservableProperty]
+ private List _ariaFileAllocations = new();
+
+ [ObservableProperty]
+ private string _selectedAriaFileAllocation = string.Empty;
+
+ #endregion
+
+ public NetworkViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated");
+ TipSettingFailed = DictionaryResource.GetString("TipSettingFailed");
+
+ #region 属性初始化
+
+ // builtin同时下载数
+ for (int i = 1; i <= 10; i++) { MaxCurrentDownloads.Add(i); }
+
+ // builtin最大线程数
+ for (int i = 1; i <= 10; i++) { Splits.Add(i); }
+
+ // Aria的日志等级
+ AriaLogLevels = new List
+ {
+ "DEBUG",
+ "INFO",
+ "NOTICE",
+ "WARN",
+ "ERROR"
+ };
+
+ // Aria同时下载数
+ for (int i = 1; i <= 10; i++) { AriaMaxConcurrentDownloads.Add(i); }
+
+ // Aria最大线程数
+ for (int i = 1; i <= 10; i++) { AriaSplits.Add(i); }
+
+ // Aria文件预分配
+ AriaFileAllocations = new List
+ {
+ "NONE",
+ "PREALLOC",
+ "FALLOC"
+ };
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ ///
+ /// 加载页面时执行
+ ///
+ [RelayCommand]
+ private void OnLoaded()
+ {
+ IsOnNavigatedTo = true;
+
+ // 启用https
+ AllowStatus useSSL = SettingsManager.GetInstance().UseSSL();
+ UseSSL = useSSL == AllowStatus.YES;
+
+ // UserAgent
+ UserAgent = SettingsManager.GetInstance().GetUserAgent();
+
+ // 选择下载器
+ var downloader = SettingsManager.GetInstance().GetDownloader();
+ switch (downloader)
+ {
+ case Downloader.NOT_SET:
+ break;
+ case Downloader.BUILT_IN:
+ Builtin = true;
+ break;
+ case Downloader.ARIA:
+ Aria2c = true;
+ break;
+ case Downloader.CUSTOM_ARIA:
+ CustomAria2c = true;
+ break;
+ }
+
+ // builtin同时下载数
+ SelectedMaxCurrentDownload = SettingsManager.GetInstance().GetMaxCurrentDownloads();
+
+ // builtin最大线程数
+ SelectedSplit = SettingsManager.GetInstance().GetSplit();
+
+ // 是否开启builtin http代理
+ AllowStatus isHttpProxy = SettingsManager.GetInstance().IsHttpProxy();
+ IsHttpProxy = isHttpProxy == AllowStatus.YES;
+
+ // builtin的http代理的地址
+ HttpProxy = SettingsManager.GetInstance().GetHttpProxy();
+
+ // builtin的http代理的端口
+ HttpProxyPort = SettingsManager.GetInstance().GetHttpProxyListenPort();
+
+ // Aria服务器host
+ AriaHost = SettingsManager.GetInstance().GetAriaHost();
+
+ // Aria服务器端口
+ AriaListenPort = SettingsManager.GetInstance().GetAriaListenPort();
+
+ // Aria服务器Token
+ AriaToken = SettingsManager.GetInstance().GetAriaToken();
+
+ // Aria的日志等级
+ AriaConfigLogLevel ariaLogLevel = SettingsManager.GetInstance().GetAriaLogLevel();
+ SelectedAriaLogLevel = ariaLogLevel.ToString("G");
+
+ // Aria同时下载数
+ SelectedAriaMaxConcurrentDownload = SettingsManager.GetInstance().GetMaxCurrentDownloads();
+
+ // Aria最大线程数
+ SelectedAriaSplit = SettingsManager.GetInstance().GetAriaSplit();
+
+ // Aria下载速度限制
+ AriaMaxOverallDownloadLimit = SettingsManager.GetInstance().GetAriaMaxOverallDownloadLimit();
+
+ // Aria下载单文件速度限制
+ AriaMaxDownloadLimit = SettingsManager.GetInstance().GetAriaMaxDownloadLimit();
+
+ // 是否开启Aria http代理
+ AllowStatus isAriaHttpProxy = SettingsManager.GetInstance().IsAriaHttpProxy();
+ IsAriaHttpProxy = isAriaHttpProxy == AllowStatus.YES;
+
+ // Aria的http代理的地址
+ AriaHttpProxy = SettingsManager.GetInstance().GetAriaHttpProxy();
+
+ // Aria的http代理的端口
+ AriaHttpProxyPort = SettingsManager.GetInstance().GetAriaHttpProxyListenPort();
+
+ // Aria文件预分配
+ AriaConfigFileAllocation ariaFileAllocation = SettingsManager.GetInstance().GetAriaFileAllocation();
+ SelectedAriaFileAllocation = ariaFileAllocation.ToString("G");
+
+ IsOnNavigatedTo = false;
+ }
+
+ ///
+ /// 是否启用https事件
+ ///
+ [RelayCommand]
+ private void SetUseSSL()
+ {
+ AllowStatus useSSL = UseSSL ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().UseSSL(useSSL);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 设置UserAgent事件
+ ///
+ [RelayCommand]
+ private void SetUserAgent()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetUserAgent(UserAgent);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 下载器选择事件
+ ///
+ ///
+ [RelayCommand]
+ private void SelectDownloader(string parameter)
+ {
+ var downloader = parameter switch
+ {
+ "Builtin" => Downloader.BUILT_IN,
+ "Aria2c" => Downloader.ARIA,
+ "CustomAria2c" => Downloader.CUSTOM_ARIA,
+ _ => SettingsManager.GetInstance().GetDownloader(),
+ };
+ bool isSucceed = SettingsManager.GetInstance().SetDownloader(downloader);
+ PublishTip(Key, isSucceed);
+
+ // 弹窗提示是否重启程序
+ // TODO
+
+ //AlertService alertService = new AlertService(dialogService);
+ //ButtonResult result = alertService.ShowInfo(DictionaryResource.GetString("ConfirmReboot"));
+ //if (result == ButtonResult.OK)
+ //{
+ // System.Windows.Application.Current.Shutdown();
+ // System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().Location);
+ //}
+ }
+
+ ///
+ /// builtin同时下载数事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetMaxCurrentDownloads(object parameter)
+ {
+ SelectedMaxCurrentDownload = (int)parameter;
+
+ bool isSucceed = SettingsManager.GetInstance().SetMaxCurrentDownloads(SelectedMaxCurrentDownload);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// builtin最大线程数事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetSplits(object parameter)
+ {
+ SelectedSplit = (int)parameter;
+
+ bool isSucceed = SettingsManager.GetInstance().SetSplit(SelectedSplit);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否开启builtin http代理事件
+ ///
+ [RelayCommand]
+ private void SetIsHttpProxy()
+ {
+ AllowStatus isHttpProxy = IsHttpProxy ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsHttpProxy(isHttpProxy);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// builtin的http代理的地址事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetHttpProxy(string parameter)
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetHttpProxy(parameter);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// builtin的http代理的端口事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetHttpProxyPort(string parameter)
+ {
+ int httpProxyPort = (int)Number.GetInt(parameter);
+ HttpProxyPort = httpProxyPort;
+
+ bool isSucceed = SettingsManager.GetInstance().SetHttpProxyListenPort(HttpProxyPort);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria服务器host事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaHost(string parameter)
+ {
+ AriaHost = parameter;
+ bool isSucceed = SettingsManager.GetInstance().SetAriaHost(AriaHost);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria服务器端口事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaListenPort(string parameter)
+ {
+ int listenPort = (int)Number.GetInt(parameter);
+ AriaListenPort = listenPort;
+
+ bool isSucceed = SettingsManager.GetInstance().SetAriaListenPort(AriaListenPort);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria服务器token事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaToken(string parameter)
+ {
+ AriaToken = parameter;
+ bool isSucceed = SettingsManager.GetInstance().SetAriaToken(AriaToken);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria的日志等级事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaLogLevels(string parameter)
+ {
+ var ariaLogLevel = parameter switch
+ {
+ "DEBUG" => AriaConfigLogLevel.DEBUG,
+ "INFO" => AriaConfigLogLevel.INFO,
+ "NOTICE" => AriaConfigLogLevel.NOTICE,
+ "WARN" => AriaConfigLogLevel.WARN,
+ "ERROR" => AriaConfigLogLevel.ERROR,
+ _ => AriaConfigLogLevel.INFO,
+ };
+ bool isSucceed = SettingsManager.GetInstance().SetAriaLogLevel(ariaLogLevel);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria同时下载数事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaMaxConcurrentDownloads(object parameter)
+ {
+ SelectedAriaMaxConcurrentDownload = (int)parameter;
+
+ bool isSucceed = SettingsManager.GetInstance().SetMaxCurrentDownloads(SelectedAriaMaxConcurrentDownload);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria最大线程数事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaSplits(object parameter)
+ {
+ SelectedAriaSplit = (int)parameter;
+
+ bool isSucceed = SettingsManager.GetInstance().SetAriaSplit(SelectedAriaSplit);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria下载速度限制事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaMaxOverallDownloadLimit(string parameter)
+ {
+ int downloadLimit = (int)Number.GetInt(parameter);
+ AriaMaxOverallDownloadLimit = downloadLimit;
+
+ bool isSucceed = SettingsManager.GetInstance().SetAriaMaxOverallDownloadLimit(AriaMaxOverallDownloadLimit);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria下载单文件速度限制事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaMaxDownloadLimit(string parameter)
+ {
+ int downloadLimit = (int)Number.GetInt(parameter);
+ AriaMaxDownloadLimit = downloadLimit;
+
+ bool isSucceed = SettingsManager.GetInstance().SetAriaMaxDownloadLimit(AriaMaxDownloadLimit);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否开启Aria http代理事件
+ ///
+ [RelayCommand]
+ private void SetIsAriaHttpProxy()
+ {
+ AllowStatus isAriaHttpProxy = IsAriaHttpProxy ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsAriaHttpProxy(isAriaHttpProxy);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria的http代理的地址事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaHttpProxy(string parameter)
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetAriaHttpProxy(parameter);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria的http代理的端口事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaHttpProxyPort(string parameter)
+ {
+ int httpProxyPort = (int)Number.GetInt(parameter);
+ AriaHttpProxyPort = httpProxyPort;
+
+ bool isSucceed = SettingsManager.GetInstance().SetAriaHttpProxyListenPort(AriaHttpProxyPort);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// Aria文件预分配事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAriaFileAllocations(string parameter)
+ {
+ var ariaFileAllocation = parameter switch
+ {
+ "NONE" => AriaConfigFileAllocation.NONE,
+ "PREALLOC" => AriaConfigFileAllocation.PREALLOC,
+ "FALLOC" => AriaConfigFileAllocation.FALLOC,
+ _ => AriaConfigFileAllocation.PREALLOC,
+ };
+ bool isSucceed = SettingsManager.GetInstance().SetAriaFileAllocation(ariaFileAllocation);
+ PublishTip(Key, isSucceed);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/SettingsViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/SettingsViewModel.cs
new file mode 100644
index 0000000..3e5df42
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/SettingsViewModel.cs
@@ -0,0 +1,50 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Models;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public partial class SettingsViewModel : ViewModelBase
+{
+ public const string Key = "Settings";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private int _selectedTabId = -1;
+
+ [ObservableProperty]
+ private List _tabHeaders = new();
+
+ #endregion
+
+ public SettingsViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+
+ TabHeaders.Add(new TabHeader { Id = 0, Title = DictionaryResource.GetString("Basic") });
+ TabHeaders.Add(new TabHeader { Id = 1, Title = DictionaryResource.GetString("Network") });
+ TabHeaders.Add(new TabHeader { Id = 2, Title = DictionaryResource.GetString("Video") });
+ TabHeaders.Add(new TabHeader { Id = 3, Title = DictionaryResource.GetString("SettingDanmaku") });
+ TabHeaders.Add(new TabHeader { Id = 4, Title = DictionaryResource.GetString("About") });
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task BackwardAsync()
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ };
+
+ await NavigationService.BackwardAsync(parameter);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Settings/VideoViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/VideoViewModel.cs
new file mode 100644
index 0000000..6a41779
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Settings/VideoViewModel.cs
@@ -0,0 +1,529 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Bili.Models;
+using Downkyi.Core.Bili.Utils;
+using Downkyi.Core.FileName;
+using Downkyi.Core.Settings;
+using Downkyi.Core.Settings.Enum;
+using Downkyi.Core.Settings.Models;
+using Downkyi.UI.Models;
+using Downkyi.UI.Mvvm;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace Downkyi.UI.ViewModels.Settings;
+
+public partial class VideoViewModel : BaseSettingsViewModel
+{
+ public const string Key = "Settings_Video";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private List _videoCodecs = new();
+
+ [ObservableProperty]
+ private Quality? _selectedVideoCodec = new();
+
+ [ObservableProperty]
+ private List _videoQualityList = new();
+
+ [ObservableProperty]
+ private Quality? _selectedVideoQuality = new();
+
+ [ObservableProperty]
+ private List _audioQualityList = new();
+
+ [ObservableProperty]
+ private Quality? _selectedAudioQuality = new();
+
+ [ObservableProperty]
+ private bool _isTranscodingFlvToMp4;
+
+ [ObservableProperty]
+ private bool _isUseDefaultDirectory;
+
+ [ObservableProperty]
+ private string _saveVideoDirectory = string.Empty;
+
+ [ObservableProperty]
+ private bool _downloadAll;
+
+ [ObservableProperty]
+ private bool _downloadAudio;
+
+ [ObservableProperty]
+ private bool _downloadVideo;
+
+ [ObservableProperty]
+ private bool _downloadDanmaku;
+
+ [ObservableProperty]
+ private bool _downloadSubtitle;
+
+ [ObservableProperty]
+ private bool _downloadCover;
+
+ [ObservableProperty]
+ private ObservableCollection _selectedFileName = new();
+
+ [ObservableProperty]
+ private List _optionalFields = new();
+
+ [ObservableProperty]
+ private int _selectedOptionalField;
+
+ [ObservableProperty]
+ private List _timeFormatList = new();
+
+ [ObservableProperty]
+ private string _selectedTimeFormat = string.Empty;
+
+ [ObservableProperty]
+ private List _orderFormatList = new();
+
+ [ObservableProperty]
+ private OrderFormatDisplay? _selectedOrderFormat = new();
+
+ #endregion
+
+ public VideoViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated");
+ TipSettingFailed = DictionaryResource.GetString("TipSettingFailed");
+
+ #region 属性初始化
+
+ // 优先下载的视频编码
+ VideoCodecs = QualityList.GetCodecIds();
+
+ // 优先下载画质
+ VideoQualityList = QualityList.GetResolutions();
+
+ // 优先下载音质
+ AudioQualityList = QualityList.GetAudioQualities();
+ AudioQualityList[3].Id = AudioQualityList[3].Id + 1000;
+ AudioQualityList[4].Id = AudioQualityList[4].Id + 1000;
+
+ // 文件命名中的序号格式
+ OrderFormatList.Add(new OrderFormatDisplay { Id = Core.Settings.Enum.OrderFormat.NATURAL, Name = DictionaryResource.GetString("OrderFormatNatural") });
+ OrderFormatList.Add(new OrderFormatDisplay { Id = Core.Settings.Enum.OrderFormat.LEADING_ZEROS, Name = DictionaryResource.GetString("OrderFormatLeadingZeros") });
+
+ // 文件命名格式
+ SelectedFileName.CollectionChanged += new NotifyCollectionChangedEventHandler((sender, e) =>
+ {
+ // 当前显示的命名格式part
+ var fileName = new List();
+ foreach (var item in SelectedFileName)
+ {
+ fileName.Add(item.Id);
+ }
+
+ bool isSucceed = SettingsManager.GetInstance().SetFileNameParts(fileName);
+ PublishTip(Key, isSucceed);
+ });
+
+ foreach (FileNamePart item in Enum.GetValues(typeof(FileNamePart)))
+ {
+ string display = DisplayFileNamePart(item);
+ OptionalFields.Add(new() { Id = item, Title = display });
+ }
+
+ SelectedOptionalField = -1;
+
+ // 文件命名中的时间格式
+ TimeFormatList.Add("yyyy-MM-dd");
+ TimeFormatList.Add("yyyy.MM.dd");
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ ///
+ /// 加载页面时执行
+ ///
+ [RelayCommand]
+ private void OnLoaded()
+ {
+ IsOnNavigatedTo = true;
+
+ // 优先下载的视频编码
+ int videoCodecs = SettingsManager.GetInstance().GetVideoCodecs();
+ SelectedVideoCodec = VideoCodecs.FirstOrDefault(t => { return t.Id == videoCodecs; });
+
+ // 优先下载画质
+ int quality = SettingsManager.GetInstance().GetQuality();
+ SelectedVideoQuality = VideoQualityList.FirstOrDefault(t => { return t.Id == quality; });
+
+ // 优先下载音质
+ int audioQuality = SettingsManager.GetInstance().GetAudioQuality();
+ SelectedAudioQuality = AudioQualityList.FirstOrDefault(t => { return t.Id == audioQuality; });
+
+ // 是否下载flv视频后转码为mp4
+ AllowStatus isTranscodingFlvToMp4 = SettingsManager.GetInstance().IsTranscodingFlvToMp4();
+ IsTranscodingFlvToMp4 = isTranscodingFlvToMp4 == AllowStatus.YES;
+
+ // 是否使用默认下载目录
+ AllowStatus isUseSaveVideoRootPath = SettingsManager.GetInstance().IsUseSaveVideoRootPath();
+ IsUseDefaultDirectory = isUseSaveVideoRootPath == AllowStatus.YES;
+
+ // 默认下载目录
+ SaveVideoDirectory = SettingsManager.GetInstance().GetSaveVideoRootPath();
+
+ // 下载内容
+ VideoContentSettings videoContent = SettingsManager.GetInstance().GetVideoContent();
+
+ DownloadAudio = videoContent.DownloadAudio;
+ DownloadVideo = videoContent.DownloadVideo;
+ DownloadDanmaku = videoContent.DownloadDanmaku;
+ DownloadSubtitle = videoContent.DownloadSubtitle;
+ DownloadCover = videoContent.DownloadCover;
+
+ if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover)
+ {
+ DownloadAll = true;
+ }
+ else
+ {
+ DownloadAll = false;
+ }
+
+ // 文件命名格式
+ List fileNameParts = SettingsManager.GetInstance().GetFileNameParts();
+ SelectedFileName.Clear();
+ foreach (FileNamePart item in fileNameParts)
+ {
+ int index;
+ if (SelectedFileName.Count == 0) { index = 0; }
+ else { index = SelectedFileName.Max(it => it.Index) + 1; }
+
+ string display = DisplayFileNamePart(item);
+ SelectedFileName.Add(new FileNamePartDisplay
+ {
+ Index = index,
+ Id = item,
+ Title = display,
+ });
+ }
+
+ // 文件命名中的时间格式
+ SelectedTimeFormat = SettingsManager.GetInstance().GetFileNamePartTimeFormat();
+
+ // 文件命名中的序号格式
+ OrderFormat orderFormat = SettingsManager.GetInstance().GetOrderFormat();
+ SelectedOrderFormat = OrderFormatList.FirstOrDefault(t => { return t.Id == orderFormat; });
+
+ IsOnNavigatedTo = false;
+ }
+
+ ///
+ /// 优先下载的视频编码事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetVideoCodecs()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetVideoCodecs(SelectedVideoCodec!.Id);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 优先下载画质事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetVideoQuality()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetQuality(SelectedVideoQuality!.Id);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 优先下载音质事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetAudioQuality()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetAudioQuality(SelectedAudioQuality!.Id);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否下载flv视频后转码为mp4事件
+ ///
+ [RelayCommand]
+ private void SetIsTranscodingFlvToMp4()
+ {
+ AllowStatus isTranscodingFlvToMp4 = IsTranscodingFlvToMp4 ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsTranscodingFlvToMp4(isTranscodingFlvToMp4);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 是否使用默认下载目录事件
+ ///
+ [RelayCommand]
+ private void SetIsUseDefaultDirectory()
+ {
+ AllowStatus isUseDefaultDirectory = IsUseDefaultDirectory ? AllowStatus.YES : AllowStatus.NO;
+
+ bool isSucceed = SettingsManager.GetInstance().IsUseSaveVideoRootPath(isUseDefaultDirectory);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 修改默认下载目录事件
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task ChangeSaveVideoDirectory()
+ {
+ string directory = await StoragePicker.FolderPicker(SaveVideoDirectory);
+ if (directory == "") { return; }
+
+ bool isSucceed = SettingsManager.GetInstance().SetSaveVideoRootPath(directory);
+ PublishTip(Key, isSucceed);
+
+ if (isSucceed)
+ {
+ SaveVideoDirectory = directory;
+ }
+ }
+
+ ///
+ /// 所有内容选择事件
+ ///
+ [RelayCommand]
+ private void SetDownloadAll()
+ {
+ if (DownloadAll)
+ {
+ DownloadAudio = true;
+ DownloadVideo = true;
+ DownloadDanmaku = true;
+ DownloadSubtitle = true;
+ DownloadCover = true;
+ }
+ else
+ {
+ DownloadAudio = false;
+ DownloadVideo = false;
+ DownloadDanmaku = false;
+ DownloadSubtitle = false;
+ DownloadCover = false;
+ }
+
+ SetVideoContent();
+ }
+
+ ///
+ /// 音频选择事件
+ ///
+ [RelayCommand]
+ private void SetDownloadAudio()
+ {
+ if (!DownloadAudio)
+ {
+ DownloadAll = false;
+ }
+
+ if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover)
+ {
+ DownloadAll = true;
+ }
+
+ SetVideoContent();
+ }
+
+ ///
+ /// 视频选择事件
+ ///
+ [RelayCommand]
+ private void SetDownloadVideo()
+ {
+ if (!DownloadVideo)
+ {
+ DownloadAll = false;
+ }
+
+ if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover)
+ {
+ DownloadAll = true;
+ }
+
+ SetVideoContent();
+ }
+
+ ///
+ /// 弹幕选择事件
+ ///
+ [RelayCommand]
+ private void SetDownloadDanmaku()
+ {
+ if (!DownloadDanmaku)
+ {
+ DownloadAll = false;
+ }
+
+ if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover)
+ {
+ DownloadAll = true;
+ }
+
+ SetVideoContent();
+ }
+
+ ///
+ /// 字幕选择事件
+ ///
+ [RelayCommand]
+ private void SetDownloadSubtitle()
+ {
+ if (!DownloadSubtitle)
+ {
+ DownloadAll = false;
+ }
+
+ if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover)
+ {
+ DownloadAll = true;
+ }
+
+ SetVideoContent();
+ }
+
+ ///
+ /// 封面选择事件
+ ///
+ [RelayCommand]
+ private void SetDownloadCover()
+ {
+ if (!DownloadCover)
+ {
+ DownloadAll = false;
+ }
+
+ if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover)
+ {
+ DownloadAll = true;
+ }
+
+ SetVideoContent();
+ }
+
+ ///
+ /// 文件命名中的时间格式事件
+ ///
+ ///
+ [RelayCommand]
+ private void TimeFormat()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetFileNamePartTimeFormat(SelectedTimeFormat);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 文件命名中的序号格式事件
+ ///
+ ///
+ [RelayCommand]
+ private void OrderFormat()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetOrderFormat(SelectedOrderFormat!.Id);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 重置选中文件名字段
+ ///
+ [RelayCommand]
+ private void Reset()
+ {
+ bool isSucceed = SettingsManager.GetInstance().SetFileNameParts(null);
+ PublishTip(Key, isSucceed);
+
+ List fileNameParts = SettingsManager.GetInstance().GetFileNameParts();
+ SelectedFileName.Clear();
+ foreach (FileNamePart item in fileNameParts)
+ {
+ int index;
+ if (SelectedFileName.Count == 0) { index = 0; }
+ else { index = SelectedFileName.Max(it => it.Index) + 1; }
+
+ string display = DisplayFileNamePart(item);
+ SelectedFileName.Add(new FileNamePartDisplay
+ {
+ Index = index,
+ Id = item,
+ Title = display
+ });
+ }
+
+ SelectedOptionalField = -1;
+ }
+
+ #endregion
+
+ ///
+ /// 保存下载视频内容到设置
+ ///
+ private void SetVideoContent()
+ {
+ VideoContentSettings videoContent = new()
+ {
+ DownloadAudio = DownloadAudio,
+ DownloadVideo = DownloadVideo,
+ DownloadDanmaku = DownloadDanmaku,
+ DownloadSubtitle = DownloadSubtitle,
+ DownloadCover = DownloadCover
+ };
+
+ bool isSucceed = SettingsManager.GetInstance().SetVideoContent(videoContent);
+ PublishTip(Key, isSucceed);
+ }
+
+ ///
+ /// 可选文件名字段点击
+ ///
+ ///
+ protected void OptionalFieldsCommand(object? parameter)
+ {
+ if (parameter is not FileNamePartDisplay fileName) { return; }
+
+ int index;
+ if (SelectedFileName.Count == 0) { index = 0; }
+ else { index = SelectedFileName.Max(it => it.Index) + 1; }
+
+ SelectedFileName.Add(new FileNamePartDisplay
+ {
+ Index = index,
+ Id = fileName.Id,
+ Title = fileName.Title,
+ });
+
+ SelectedOptionalField = -1;
+ }
+
+ ///
+ /// 选中文件名字段右键点击
+ ///
+ ///
+ protected void SelectedFileNameRightButton(object? parameter)
+ {
+ if (parameter is not FileNamePartDisplay fileName) { return; }
+
+ SelectedFileName.Remove(fileName);
+
+ SelectedOptionalField = -1;
+ }
+
+ ///
+ /// 文件名字段显示
+ ///
+ ///
+ ///
+ protected virtual string DisplayFileNamePart(FileNamePart item) { return string.Empty; }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Toolbox/BiliHelperViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/BiliHelperViewModel.cs
new file mode 100644
index 0000000..c78e455
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Toolbox/BiliHelperViewModel.cs
@@ -0,0 +1,118 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.Bili.Utils;
+using Downkyi.Core.Log;
+using Downkyi.UI.Mvvm;
+using System.Diagnostics;
+
+namespace Downkyi.UI.ViewModels.Toolbox;
+
+public partial class BiliHelperViewModel : ViewModelBase
+{
+ public const string Key = "Toolbox_BiliHelper";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private string _avid = string.Empty;
+
+ [ObservableProperty]
+ private string _bvid = string.Empty;
+
+ [ObservableProperty]
+ private string _danmakuUserID = string.Empty;
+
+ [ObservableProperty]
+ private string _userMid = string.Empty;
+
+ #endregion
+
+ public BiliHelperViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+ #endregion
+ }
+
+ #region 命令申明
+
+ ///
+ /// 输入avid事件
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task SetAvid()
+ {
+ if (!ParseEntrance.IsAvId(Avid)) { return; }
+
+ long avid = ParseEntrance.GetAvId(Avid);
+ if (avid == -1) { return; }
+
+ await Task.Run(() =>
+ {
+ Bvid = BvId.Av2Bv((ulong)avid);
+ });
+ }
+
+ ///
+ /// 输入bvid事件
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task SetBvid()
+ {
+ if (!ParseEntrance.IsBvId(Bvid)) { return; }
+
+ await Task.Run(() =>
+ {
+ ulong avid = BvId.Bv2Av(Bvid);
+ Avid = $"av{avid}";
+ });
+ }
+
+ ///
+ /// 访问视频网页事件
+ ///
+ [RelayCommand]
+ private void GotoWeb()
+ {
+ string baseUrl = "https://www.bilibili.com/video/";
+ Process.Start(new ProcessStartInfo(baseUrl + Bvid) { UseShellExecute = true });
+ }
+
+ ///
+ /// 查询弹幕发送者事件
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task FindDanmakuSender()
+ {
+ await Task.Run(() =>
+ {
+ try
+ {
+ UserMid = DanmakuSender.FindDanmakuSender(DanmakuUserID);
+ }
+ catch (Exception e)
+ {
+ UserMid = string.Empty;
+
+ Log.Logger.Error(e);
+ }
+ });
+ }
+
+ ///
+ /// 访问用户空间事件
+ ///
+ [RelayCommand]
+ private void VisitUserSpace()
+ {
+ if (UserMid == string.Empty) { return; }
+
+ string baseUrl = "https://space.bilibili.com/";
+ Process.Start(new ProcessStartInfo(baseUrl + UserMid) { UseShellExecute = true });
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Toolbox/DelogoViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/DelogoViewModel.cs
new file mode 100644
index 0000000..b4696cb
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Toolbox/DelogoViewModel.cs
@@ -0,0 +1,124 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.FFmpeg;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Toolbox;
+
+public partial class DelogoViewModel : ViewModelBase
+{
+ public const string Key = "Toolbox_Delogo";
+
+ // 是否正在执行去水印任务
+ protected bool _isDelogo = false;
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private string _videoPath = string.Empty;
+
+ [ObservableProperty]
+ private int _logoWidth;
+
+ [ObservableProperty]
+ private int _logoHeight;
+
+ [ObservableProperty]
+ private int _logoX;
+
+ [ObservableProperty]
+ private int _logoY;
+
+ [ObservableProperty]
+ private string _status = string.Empty;
+
+ #endregion
+
+ public DelogoViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+
+ VideoPath = string.Empty;
+
+ LogoWidth = 0;
+ LogoHeight = 0;
+ LogoX = 0;
+ LogoY = 0;
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ ///
+ /// 选择视频事件
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task SelectVideo()
+ {
+ if (_isDelogo)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished"));
+ return;
+ }
+
+ VideoPath = await StoragePicker.SelectVideoFileAsync();
+ }
+
+ ///
+ /// 去水印事件
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task Delogo()
+ {
+ if (_isDelogo)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished"));
+ return;
+ }
+
+ if (VideoPath == "")
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipNoSeletedVideo"));
+ return;
+ }
+
+ if (LogoWidth == -1)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoWidth"));
+ return;
+ }
+ if (LogoHeight == -1)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoHeight"));
+ return;
+ }
+ if (LogoX == -1)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoX"));
+ return;
+ }
+ if (LogoY == -1)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoY"));
+ return;
+ }
+
+ // 新文件名
+ string newFileName = VideoPath.Insert(VideoPath.Length - 4, "_delogo");
+ Status = string.Empty;
+
+ await Task.Run(() =>
+ {
+ // 执行去水印程序
+ _isDelogo = true;
+ FFmpegHelper.Delogo(VideoPath, newFileName, LogoX, LogoY, LogoWidth, LogoHeight, new Action((output) =>
+ {
+ Status += output + "\n";
+ }));
+ _isDelogo = false;
+ });
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Toolbox/ExtractMediaViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/ExtractMediaViewModel.cs
new file mode 100644
index 0000000..490218a
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Toolbox/ExtractMediaViewModel.cs
@@ -0,0 +1,139 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.FFmpeg;
+using Downkyi.UI.Mvvm;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace Downkyi.UI.ViewModels.Toolbox;
+
+public partial class ExtractMediaViewModel : ViewModelBase
+{
+ public const string Key = "Toolbox_ExtractMedia";
+
+ // 是否正在执行任务
+ private bool _isExtracting = false;
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private string _videoPathsStr = string.Empty;
+
+ [ObservableProperty]
+ private ObservableCollection _videoPaths = new();
+
+ [ObservableProperty]
+ private string _status = string.Empty;
+
+ #endregion
+
+ public ExtractMediaViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ VideoPaths.CollectionChanged += new NotifyCollectionChangedEventHandler((sender, e) =>
+ {
+ VideoPathsStr = string.Join(Environment.NewLine, VideoPaths);
+ });
+ }
+
+ #region 命令申明
+
+ ///
+ /// 选择视频事件
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task SelectVideo()
+ {
+ if (_isExtracting)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished"));
+ return;
+ }
+
+ var videoPathList = await StoragePicker.SelectMultiVideoFileAsync();
+
+ VideoPaths.Clear();
+ foreach (var path in videoPathList)
+ {
+ VideoPaths.Add(path);
+ }
+ }
+
+ ///
+ /// 提取音频事件
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task ExtractAudio()
+ {
+ if (_isExtracting)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished"));
+ return;
+ }
+
+ if (VideoPaths.Count <= 0)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipNoSeletedVideo"));
+ return;
+ }
+
+ Status = string.Empty;
+
+ await Task.Run(() =>
+ {
+ _isExtracting = true;
+ foreach (var item in VideoPaths)
+ {
+ // 音频文件名
+ string audioFileName = item.Remove(item.Length - 4, 4) + ".aac";
+ // 执行提取音频程序
+ FFmpegHelper.ExtractAudio(item, audioFileName, new Action((output) =>
+ {
+ Status += output + "\n";
+ }));
+ }
+ _isExtracting = false;
+ });
+ }
+
+ ///
+ /// 提取视频事件
+ ///
+ ///
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task ExtractVideo()
+ {
+ if (_isExtracting)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished"));
+ return;
+ }
+
+ if (VideoPaths.Count <= 0)
+ {
+ NotificationEvent.Publish(DictionaryResource.GetString("TipNoSeletedVideo"));
+ return;
+ }
+
+ Status = string.Empty;
+
+ await Task.Run(() =>
+ {
+ _isExtracting = true;
+ foreach (var item in VideoPaths)
+ {
+ // 视频文件名
+ string videoFileName = item.Remove(item.Length - 4, 4) + "_onlyVideo.mp4";
+ // 执行提取视频程序
+ FFmpegHelper.ExtractVideo(item, videoFileName, new Action((output) =>
+ {
+ Status += output + "\n";
+ }));
+ }
+ _isExtracting = false;
+ });
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/Toolbox/ToolboxViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/ToolboxViewModel.cs
new file mode 100644
index 0000000..5ec213d
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/Toolbox/ToolboxViewModel.cs
@@ -0,0 +1,48 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Models;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.Toolbox;
+
+public partial class ToolboxViewModel : ViewModelBase
+{
+ public const string Key = "Toolbox";
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private int _selectedTabId = -1;
+
+ [ObservableProperty]
+ private List _tabHeaders = new();
+
+ #endregion
+
+ public ToolboxViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+
+ TabHeaders.Add(new TabHeader { Id = 0, Title = DictionaryResource.GetString("BiliHelper") });
+ TabHeaders.Add(new TabHeader { Id = 1, Title = DictionaryResource.GetString("Delogo") });
+ TabHeaders.Add(new TabHeader { Id = 2, Title = DictionaryResource.GetString("ExtractMedia") });
+
+ #endregion
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task BackwardAsync()
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ };
+
+ await NavigationService.BackwardAsync(parameter);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/User/MySpaceViewModel.cs b/src/Downkyi.UI/ViewModels/User/MySpaceViewModel.cs
new file mode 100644
index 0000000..0f30bf0
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/User/MySpaceViewModel.cs
@@ -0,0 +1,29 @@
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.User;
+
+public partial class MySpaceViewModel : ViewModelBase
+{
+ public const string Key = "MySpace";
+
+ public MySpaceViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task BackwardAsync()
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ };
+
+ await NavigationService.BackwardAsync(parameter);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.UI/ViewModels/User/UserSpaceViewModel.cs b/src/Downkyi.UI/ViewModels/User/UserSpaceViewModel.cs
new file mode 100644
index 0000000..73131a0
--- /dev/null
+++ b/src/Downkyi.UI/ViewModels/User/UserSpaceViewModel.cs
@@ -0,0 +1,29 @@
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Mvvm;
+
+namespace Downkyi.UI.ViewModels.User;
+
+public partial class UserSpaceViewModel : ViewModelBase
+{
+ public const string Key = "UserSpace";
+
+ public UserSpaceViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ }
+
+ #region 命令申明
+
+ [RelayCommand(FlowExceptionsToTaskScheduler = true)]
+ private async Task BackwardAsync()
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ };
+
+ await NavigationService.BackwardAsync(parameter);
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/src/Downkyi.sln b/src/Downkyi.sln
new file mode 100644
index 0000000..8685ada
--- /dev/null
+++ b/src/Downkyi.sln
@@ -0,0 +1,49 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33424.131
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downkyi", "Downkyi\Downkyi.csproj", "{4021D838-C609-49A7-9065-3AC23EBDBC54}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downkyi.UI", "Downkyi.UI\Downkyi.UI.csproj", "{7FCEED5F-BF98-44B0-8F4D-594C182D235F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downkyi.Core", "Downkyi.Core\Downkyi.Core.csproj", "{5CED42DB-2155-45D0-B195-57BB894CB97B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BiliSharp", "BiliSharp\BiliSharp.csproj", "{199B3491-51F0-460D-AB90-191B7DD377D6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BiliSharp.UnitTest", "BiliSharp.UnitTest\BiliSharp.UnitTest.csproj", "{107D1B61-6936-45A0-B4AF-3776C736649A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {4021D838-C609-49A7-9065-3AC23EBDBC54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4021D838-C609-49A7-9065-3AC23EBDBC54}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4021D838-C609-49A7-9065-3AC23EBDBC54}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4021D838-C609-49A7-9065-3AC23EBDBC54}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5CED42DB-2155-45D0-B195-57BB894CB97B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5CED42DB-2155-45D0-B195-57BB894CB97B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5CED42DB-2155-45D0-B195-57BB894CB97B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5CED42DB-2155-45D0-B195-57BB894CB97B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {199B3491-51F0-460D-AB90-191B7DD377D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {199B3491-51F0-460D-AB90-191B7DD377D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {199B3491-51F0-460D-AB90-191B7DD377D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {199B3491-51F0-460D-AB90-191B7DD377D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {107D1B61-6936-45A0-B4AF-3776C736649A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {107D1B61-6936-45A0-B4AF-3776C736649A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {107D1B61-6936-45A0-B4AF-3776C736649A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {107D1B61-6936-45A0-B4AF-3776C736649A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {32C09F62-DE01-4EBF-A0FA-6987F8C28C56}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Downkyi/.gitignore b/src/Downkyi/.gitignore
new file mode 100644
index 0000000..8afdcb6
--- /dev/null
+++ b/src/Downkyi/.gitignore
@@ -0,0 +1,454 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/src/Downkyi/App.axaml b/src/Downkyi/App.axaml
new file mode 100644
index 0000000..fce40ba
--- /dev/null
+++ b/src/Downkyi/App.axaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/App.axaml.cs b/src/Downkyi/App.axaml.cs
new file mode 100644
index 0000000..164cad6
--- /dev/null
+++ b/src/Downkyi/App.axaml.cs
@@ -0,0 +1,78 @@
+using AsyncImageLoader;
+using AsyncImageLoader.Loaders;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Downkyi.UI.Services;
+using Downkyi.Views;
+
+namespace Downkyi;
+
+public partial class App : Application
+{
+ public App()
+ {
+ }
+
+ public override void Initialize()
+ {
+ // Register handle task exception
+ HandleTaskException.RegisterEvents();
+
+ // Register services
+ ServiceLocator.ConfigureServices();
+
+ // ͼΪ
+ ImageLoader.AsyncImageLoader = new DiskCachedWebImageLoader();
+
+ //
+ Ioc.Default.GetRequiredService().LoadTheme("Default");
+
+ // л
+ Ioc.Default.GetRequiredService().LoadLanguage("Default");
+
+ AvaloniaXamlLoader.Load(this);
+
+ var accentColor = Color.Parse(Ioc.Default.GetRequiredService().GetColor("ColorPrimary"));
+ Resources["SystemAccentColor"] = accentColor;
+ //Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
+ //Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
+ //Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
+ //Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3);
+ //Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5);
+ //Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7);
+
+ //var hsl = Color.ToHsl(accentColor);
+ //HslColor.FromHsl(hsl.H, hsl.S, 0.7);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ // Line below is needed to remove Avalonia data validation.
+ // Without this line you will get duplicate validations from both Avalonia and CT
+ //ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin);
+ //desktop.MainWindow = new MainWindow
+ //{
+ // DataContext = ServiceLocator.MainWindowViewModel,
+ //};
+ desktop.MainWindow = new SplashWindow(() =>
+ {
+ var mainWindow = new MainWindow()
+ {
+ DataContext = ServiceLocator.MainWindowViewModel,
+ };
+
+ mainWindow.Show();
+ mainWindow.Focus();
+
+ desktop.MainWindow = mainWindow;
+ });
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Assets/Fonts/SourceHanSansCN-Bold.otf b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Bold.otf
new file mode 100644
index 0000000..8e1e869
Binary files /dev/null and b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Bold.otf differ
diff --git a/src/Downkyi/Assets/Fonts/SourceHanSansCN-Normal.otf b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Normal.otf
new file mode 100644
index 0000000..abd36ce
Binary files /dev/null and b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Normal.otf differ
diff --git a/src/Downkyi/Assets/bili/default_header.jpg b/src/Downkyi/Assets/bili/default_header.jpg
new file mode 100644
index 0000000..5e5ea0b
Binary files /dev/null and b/src/Downkyi/Assets/bili/default_header.jpg differ
diff --git a/src/Downkyi/Assets/bili/login/login_top_bar.png b/src/Downkyi/Assets/bili/login/login_top_bar.png
new file mode 100644
index 0000000..68dc5d3
Binary files /dev/null and b/src/Downkyi/Assets/bili/login/login_top_bar.png differ
diff --git a/src/Downkyi/Assets/bili/login/qrcode_login_2233.png b/src/Downkyi/Assets/bili/login/qrcode_login_2233.png
new file mode 100644
index 0000000..3d9ff56
Binary files /dev/null and b/src/Downkyi/Assets/bili/login/qrcode_login_2233.png differ
diff --git a/src/Downkyi/Assets/bili/login/qrcode_login_tip.png b/src/Downkyi/Assets/bili/login/qrcode_login_tip.png
new file mode 100644
index 0000000..177018b
Binary files /dev/null and b/src/Downkyi/Assets/bili/login/qrcode_login_tip.png differ
diff --git a/src/Downkyi/Assets/bili/login/scan_succeed.png b/src/Downkyi/Assets/bili/login/scan_succeed.png
new file mode 100644
index 0000000..58151a1
Binary files /dev/null and b/src/Downkyi/Assets/bili/login/scan_succeed.png differ
diff --git a/src/Downkyi/Assets/bili/下载.png b/src/Downkyi/Assets/bili/下载.png
new file mode 100644
index 0000000..58151a1
Binary files /dev/null and b/src/Downkyi/Assets/bili/下载.png differ
diff --git a/src/Downkyi/Assets/logo.ico b/src/Downkyi/Assets/logo.ico
new file mode 100644
index 0000000..0145cb9
Binary files /dev/null and b/src/Downkyi/Assets/logo.ico differ
diff --git a/src/Downkyi/Behaviors/FileNamePartDropHandler.cs b/src/Downkyi/Behaviors/FileNamePartDropHandler.cs
new file mode 100644
index 0000000..8466cef
--- /dev/null
+++ b/src/Downkyi/Behaviors/FileNamePartDropHandler.cs
@@ -0,0 +1,80 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.VisualTree;
+using Avalonia.Xaml.Interactions.DragAndDrop;
+using Downkyi.UI.Models;
+using Downkyi.ViewModels.Settings;
+
+namespace Downkyi.Behaviors;
+
+public class FileNamePartDropHandler : DropHandlerBase
+{
+ private bool Validate(ItemsControl list, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute) where T : FileNamePartDisplay
+ {
+ if (sourceContext is not T sourceItem
+ || targetContext is not VideoViewModelProxy vm
+ || list.GetVisualAt(e.GetPosition(list)) is not Control targetControl
+ || targetControl.DataContext is not T targetItem)
+ {
+ return false;
+ }
+
+ var items = vm.SelectedFileName;
+ var sourceIndex = items.IndexOf(sourceItem);
+ var targetIndex = items.IndexOf(targetItem);
+
+ if (sourceIndex < 0 || targetIndex < 0)
+ {
+ return false;
+ }
+
+ switch (e.DragEffects)
+ {
+ case DragDropEffects.Copy:
+ {
+ if (bExecute)
+ {
+ var clone = new FileNamePartDisplay() { Title = sourceItem.Title + "_copy" };
+ InsertItem(items, clone, targetIndex + 1);
+ }
+ return true;
+ }
+ case DragDropEffects.Move:
+ {
+ if (bExecute)
+ {
+ MoveItem(items, sourceIndex, targetIndex);
+ }
+ return true;
+ }
+ case DragDropEffects.Link:
+ {
+ if (bExecute)
+ {
+ SwapItem(items, sourceIndex, targetIndex);
+ }
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
+ {
+ if (e.Source is Control && sender is ItemsControl list)
+ {
+ return Validate(list, e, sourceContext, targetContext, false);
+ }
+ return false;
+ }
+
+ public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
+ {
+ if (e.Source is Control && sender is ItemsControl list)
+ {
+ return Validate(list, e, sourceContext, targetContext, true);
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Converter/IntEquealConverter.cs b/src/Downkyi/Converter/IntEquealConverter.cs
new file mode 100644
index 0000000..99ec0b0
--- /dev/null
+++ b/src/Downkyi/Converter/IntEquealConverter.cs
@@ -0,0 +1,34 @@
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using System;
+using System.Globalization;
+
+namespace Downkyi.Converter;
+
+public class IntEquealConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (parameter is string str)
+ {
+ if (value is int source && int.TryParse(str, out int target))
+ {
+ return source == target;
+ }
+ }
+ else if (parameter is int integer)
+ {
+ if (value is int source && targetType.IsAssignableTo(typeof(bool)))
+ {
+ return source == integer;
+ }
+ }
+
+ return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Converter/StringToResourceConverter.cs b/src/Downkyi/Converter/StringToResourceConverter.cs
new file mode 100644
index 0000000..e84598d
--- /dev/null
+++ b/src/Downkyi/Converter/StringToResourceConverter.cs
@@ -0,0 +1,28 @@
+using Avalonia;
+using Avalonia.Data.Converters;
+using Downkyi.Services;
+using System;
+using System.Globalization;
+
+namespace Downkyi.Converter;
+
+///
+/// 通过字符串访问Resource资源
+///
+public class StringToResourceConverter : IValueConverter
+{
+ public static StringToResourceConverter Instance { get; } = new();
+
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value == null)
+ return null;
+
+ return DictionaryResource.GetResource(Application.Current?.Resources, value.ToString()!);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Downkyi.csproj b/src/Downkyi/Downkyi.csproj
new file mode 100644
index 0000000..170e943
--- /dev/null
+++ b/src/Downkyi/Downkyi.csproj
@@ -0,0 +1,58 @@
+
+
+ WinExe
+ net6.0
+ enable
+ true
+ Downkyi
+ app.manifest
+ Assets\logo.ico
+ DownKyi
+ DownKyi
+ DownKyi
+ DownKyi
+ DownKyi
+ Copyright © Downkyi 2020-2023
+ 2.0.0
+ 2.0.0
+ 2.0.0
+ 2.0.0
+
+
+ none
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/HandleTaskException.cs b/src/Downkyi/HandleTaskException.cs
new file mode 100644
index 0000000..b43b963
--- /dev/null
+++ b/src/Downkyi/HandleTaskException.cs
@@ -0,0 +1,69 @@
+using Downkyi.Core.Log;
+using NLog;
+using System;
+using System.Threading.Tasks;
+
+namespace Downkyi;
+
+public static class HandleTaskException
+{
+ public static void RegisterEvents()
+ {
+ // Task线程内未捕获异常处理事件
+ TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
+
+ // 非UI线程未捕获异常处理事件(例如自己创建的一个子线程)
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
+
+ }
+
+ // Task线程内未捕获异常处理事件
+ private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
+ {
+ try
+ {
+ if (e.Exception is Exception exception)
+ {
+ HandleException(exception);
+ }
+ }
+ catch (Exception ex)
+ {
+ HandleException(ex);
+ }
+ finally
+ {
+ e.SetObserved();
+ // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
+ LogManager.Shutdown();
+ }
+ }
+
+ // 非UI线程未捕获异常处理事件(例如自己创建的一个子线程)
+ private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ try
+ {
+ if (e.ExceptionObject is Exception exception)
+ {
+ HandleException(exception);
+ }
+ }
+ catch (Exception ex)
+ {
+ HandleException(ex);
+ }
+ finally
+ {
+ // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
+ LogManager.Shutdown();
+ }
+ }
+
+ // 日志记录
+ private static void HandleException(Exception e)
+ {
+ Log.Logger.Fatal(e, "UnhandleException");
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/Images/Navigation.axaml b/src/Downkyi/Images/Navigation.axaml
new file mode 100644
index 0000000..3fe2b85
--- /dev/null
+++ b/src/Downkyi/Images/Navigation.axaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Images/Normal.axaml b/src/Downkyi/Images/Normal.axaml
new file mode 100644
index 0000000..2adcba2
--- /dev/null
+++ b/src/Downkyi/Images/Normal.axaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/Downkyi/Languages/Default.axaml b/src/Downkyi/Languages/Default.axaml
new file mode 100644
index 0000000..d04c36b
--- /dev/null
+++ b/src/Downkyi/Languages/Default.axaml
@@ -0,0 +1,370 @@
+
+
+ zh-cn
+
+
+ 哔哩下载姬
+ 未知
+
+
+ 关闭
+ 最大化
+ 最小化
+ 还原
+ 皮肤
+
+
+ 登录
+ 请输入B站网址链接或av、BV号等……
+ 请输入B站视频播放地址……
+ 设置
+ 下载管理
+ 工具箱
+
+
+ 二维码
+ Cookies
+
+ 扫码成功
请在手机上确认
+ 扫描二维码登录
+ 请使用哔哩哔哩客户端
扫码登录
或扫码下载APP
+ 获取登录链接失败!
+ 密钥错误!尝试重新获取!
+ 已超时!尝试重新获取二维码!
+ 登录成功!(3秒后自动回到主页)
+ 未保存登录信息!(3秒后自动回到主页)
+
+ 请输入Cookies
+ 保存
+ 登录成功!
+ 未保存登录信息!
+
+
+ 我的个人空间
+ 退出登录
+ 请稍等,马上就好~
+ 等级
+ 关注数
+ 悄悄关注数
+ 粉丝数
+ 黑名单数
+ 节操值
+ 封禁状态
+ 收藏夹
+ 我的订阅
+ 稍后再看
+ 历史记录
+
+ 正常
+ 封停
+
+
+ 个人空间
+ 已关注
+ 未关注
+ 关注数
+ 粉丝数
+ 获赞数
+ 播放数
+ 阅读数
+ 全部
+ 投稿视频
+ 频道
+ 合集和列表
+ 请稍等,马上就好~
+
+
+ 收藏夹
+ 个内容
+
+
+ 播放:
+ 收藏:
+ UP主:
+ 投稿:
+ 收藏于:
+
+
+ 我的订阅
+ 追番
+ 追剧
+ 尚未观看
+
+
+ 历史记录
+ 已看完
+ 刚开始
+ 看到
+
+
+ 稍后再看
+
+
+ 下载选中项
+ 下载全部
+ 请稍等,马上就好~
+
+
+ 好友
+ 关注
+ 粉丝
+
+ 请稍等,马上就好~
+ 请稍等,马上就好~
+
+ 全部关注
+ 悄悄关注
+
+
+ 复制封面图片
+ 复制封面URL
+ 播放
+ 弹幕
+ 点赞
+ 投币
+ 收藏
+ 分享
+ 评论
+
+ 番剧
+ 电影
+ 纪录片
+ 国创
+ 电视剧
+ 综艺
+ 课程
+
+ 序号
+ 名称
+ 时长
+ 音质
+ 画质
+ 视频编码
+
+ 全选
+ 解析
+ 解析视频
+ 下载选中项
+ 下载全部
+
+ 已经添加到下载列表~
+ 已经下载完成~
+ 没有选中项符合下载要求!
+ 成功添加了
+ 项~
+
+
+ 正在下载
+ 已下载
+
+ 音频
+ 视频
+ 弹幕
+ 字幕
+ 封面
+ 正在解析……
+ 下载中……
+ 混流中……
+ 暂停中……
+ 等待中……
+ 下载失败
+
+ 正在下载
+ 个视频!
+ 全部暂停
+ 全部开始
+ 全部删除
+
+ 已下载
+ 个视频!
+ 按下载先后排序
+ 按序号排序
+ 清空所有记录
+
+ 开始
+ 暂停
+ 重试
+ 移除
+ 打开文件夹
+ 播放
+
+
+ 按回车键应用设置
+
+ 基本
+ 下载完成后的动作:
+ 无
+ 关闭程序
+ 关闭计算机
+ 监听剪贴板
+ 视频自动解析
+ 视频解析范围:
+ 解析后自动下载已解析视频
+
+ 网络
+ 启用https(若下载器提示SSL错误,则关闭此项)
+ UserAgent:
+ 选择下载器(重启生效):
+ 内建下载器
+ Aria2下载器
+ 自定义Aria2下载器
+ Aria服务器地址:
+ Aria服务器端口:
+ Aria服务器Token:
+ Aria日志等级:
+ Aria同时下载数:
+ Aria最大线程数:
+ Aria下载速度限制(KB/s)
+ 全局下载速度限制[0: 无限制]
+ 单任务下载速度限制[0: 无限制]
+ Aria文件预分配:
+ 使用Http代理
+ 代理地址:
+ 端口:
+ 同时下载数:
+ 最大线程数:
+
+ 视频
+ 优先下载的视频编码:
+ 优先下载的视频画质:
+ 优先下载的视频音质:
+ 下载FLV视频后转码为mp4
+ 使用默认下载目录
+ 默认下载目录:
+ 默认将文件下载到该文件夹中
+ 更改目录
+ 文件命名格式
+ 文件名:
+ 可选字段:
+ 序号
+ 视频章节
+ 视频标题
+ 分P标题
+ 视频分区
+ 音质
+ 画质
+ 视频编码
+ 发布时间
+ 空格
+ UP主ID
+ UP主昵称
+ 恢复默认
+ 时间格式:
+ 序号格式:
+ 自然数
+ 前导零填充
+
+ 弹幕
+ 按类型屏蔽
+ 顶部
+ 底部
+ 滚动
+ 分辨率:
+ 字体名称:
+ 字体大小:
+ 限制行数:
+ 布局算法:
+ 速度同步
+ 每一条滚动弹幕的移动速度都是相同的
+ 速度异步
+ 弹幕滚动速度取决于它的文本长度
+
+ 关于
+ 访问主页
+ 当前版本:
+ 检查更新
+ 意见反馈
+ 接收测试版更新
+ 启动时检查更新
+ 设置已更新~
+ 设置失败!
+ 第三方组件
+ 名称
+ 作者
+ 版本
+ 许可证
+ 免责申明
+ 1.本软件只提供视频解析,不提供任何资源上传、存储到服务器的功能。
+ 2.本软件仅解析来自B站的内容,不会对解析到的音视频进行二次编码,部分视频会进行有限的格式转换、拼接等操作。
+ 3.本软件解析得到的所有内容均来自B站UP主上传、分享,其版权均归原作者所有。内容提供者、上传者(UP主)应对其提供、上传的内容承担全部责任。
+ 4.本软件提供的所有资源,仅可用作学习交流使用,未经原作者授权,禁止用于其他用途。请在下载24小时内删除。为尊重作者版权,请前往资源的原始发布网站观看,支持原创,谢谢。
+ 5.任何涉及商业盈利目的均不得使用,否则产生的一切后果将由您自己承担。
+ 6.因使用本软件产生的版权问题,软件作者概不负责。
+
+
+ B站助手
+ AV和BV互转
+ AV号形如"av123",BV号形如"BVxxx"。
+ AVID:
+ BVID:
+ 访问网页
+ 弹幕中的userID:
+ 查询弹幕发送者->
+
+ 去水印
+ 视频地址:
+ 选择视频
+ 水印尺寸与位置(左上角为原点,单位:像素)
+ 宽:
+ 高:
+ X:
+ Y:
+ 请等待当前任务完成!
+ 没有选择视频!
+ 请输入正确的宽度(正整数)!
+ 请输入正确的高度(正整数)!
+ 请输入正确的X(正整数)!
+ 请输入正确的Y(正整数)!
+ 输出信息:
+
+ 音视频分离
+ 提取音频
+ 提取视频
+
+
+ 信息
+ 警告
+ 错误
+
+ 确定
+ 取消
+
+ 此项需重启生效,您确定要重新启动吗?
+ 您确定要删除吗?
+ 选择文件夹
+ 选择视频
+
+ 路径错误!
+
+
+ 下载设置
+ 位置:
+ 浏览
+ 盘剩余空间:
+ 下载内容:
+ 下载内容
+ 所有
+ 音频
+ 视频
+ 弹幕
+ 字幕
+ 封面
+ 设此文件夹为默认下载文件夹
+ 选中后下次将不会弹出此窗口
+ 下载
+ 文件夹路径不能为空
+ 该磁盘不存在
+
+
+ 选择解析项
+ 无
+ 解析选中项
+ 解析当前页视频
+ 解析所有视频
+ 设为默认,下次不再弹出
+
+
diff --git a/src/Downkyi/Models/AppInfo.cs b/src/Downkyi/Models/AppInfo.cs
new file mode 100644
index 0000000..372ac7d
--- /dev/null
+++ b/src/Downkyi/Models/AppInfo.cs
@@ -0,0 +1,54 @@
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Downkyi.Services;
+using Downkyi.UI.Services;
+using System;
+using System.Text.RegularExpressions;
+
+namespace Downkyi.Models;
+
+public class AppInfo
+{
+ public string Name { get; } = Ioc.Default.GetRequiredService().GetString("AppName");
+ public long VersionCode { get; }
+ public string VersionName { get; }
+
+ const int a = 2;
+ const int b = 0;
+ const int c = 0;
+
+ public AppInfo()
+ {
+ VersionCode = a * 10000 + b * 100 + c;
+
+#if DEBUG
+ VersionName = $"{a}.{b}.{c}-debug";
+#else
+ VersionName = $"{a}.{b}.{c}";
+#endif
+ }
+
+ public static int VersionNameToCode(string versionName)
+ {
+ int code = 0;
+
+ var isMatch = Regex.IsMatch(versionName, @"^v?([1-9]\d|\d).([1-9]\d|\d).([1-9]\d|\d)$");
+ if (!isMatch)
+ {
+ return 0;
+ }
+
+ string pattern = @"([1-9]\d|\d)";
+ var m = Regex.Matches(versionName, pattern);
+ if (m.Count == 3)
+ {
+ int i = 2;
+ foreach (var item in m)
+ {
+ code += int.Parse(item == null ? "" : item.ToString()!) * (int)Math.Pow(100, i);
+ i--;
+ }
+ }
+
+ return code;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Program.cs b/src/Downkyi/Program.cs
new file mode 100644
index 0000000..ebd9b7c
--- /dev/null
+++ b/src/Downkyi/Program.cs
@@ -0,0 +1,27 @@
+using Avalonia;
+using NLog;
+using System;
+
+namespace Downkyi;
+
+internal class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) =>
+ BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ {
+ //GC.KeepAlive(typeof(SvgImageExtension).Assembly);
+ //GC.KeepAlive(typeof(Avalonia.Svg.Skia.Svg).Assembly);
+
+ return AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace();
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Roots.xml b/src/Downkyi/Roots.xml
new file mode 100644
index 0000000..9248c6f
--- /dev/null
+++ b/src/Downkyi/Roots.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/Downkyi/ServiceLocator.cs b/src/Downkyi/ServiceLocator.cs
new file mode 100644
index 0000000..7f0d059
--- /dev/null
+++ b/src/Downkyi/ServiceLocator.cs
@@ -0,0 +1,123 @@
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Downkyi.Services;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.Services;
+using Downkyi.UI.Services.Event;
+using Downkyi.UI.ViewModels;
+using Downkyi.UI.ViewModels.DownloadManager;
+using Downkyi.UI.ViewModels.Login;
+using Downkyi.UI.ViewModels.Settings;
+using Downkyi.UI.ViewModels.Toolbox;
+using Downkyi.UI.ViewModels.User;
+using Downkyi.ViewModels;
+using Downkyi.ViewModels.Login;
+using Downkyi.ViewModels.Settings;
+using Downkyi.ViewModels.Toolbox;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Downkyi;
+
+public static class ServiceLocator
+{
+ private static bool _initialized;
+
+ // 主窗体
+ public static MainWindowViewModel MainWindowViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 主页
+ public static IndexViewModel IndexViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 登录
+ public static LoginViewModel LoginViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static QRCodeViewModelProxy QRCodeViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static CookiesViewModel CookiesViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 我的个人空间
+ public static MySpaceViewModel MySpaceViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 个人空间
+ public static UserSpaceViewModel UserSpaceViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 设置
+ public static SettingsViewModel SettingsViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static BasicViewModel BasicViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static NetworkViewModel NetworkViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static VideoViewModelProxy VideoViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static DanmakuViewModelProxy DanmakuViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static AboutViewModelProxy AboutViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 下载管理
+ public static DownloadManagerViewModel DownloadManagerViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static DownloadingViewModel DownloadingViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static DownloadFinishedViewModel DownloadFinishedViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ // 工具箱
+ public static ToolboxViewModel ToolboxViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static BiliHelperViewModel BiliHelperViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static DelogoViewModelProxy DelogoViewModel =>
+ Ioc.Default.GetRequiredService();
+ public static ExtractMediaViewModelProxy ExtractMediaViewModel =>
+ Ioc.Default.GetRequiredService();
+
+ ///
+ /// Configures the services for the application.
+ ///
+ public static void ConfigureServices()
+ {
+ // Register services
+ if (!_initialized)
+ {
+ _initialized = true;
+ Ioc.Default.ConfigureServices(
+ new ServiceCollection()
+ //Services
+ .AddScoped()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ //ViewModels
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .BuildServiceProvider());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Services/DictionaryResource.cs b/src/Downkyi/Services/DictionaryResource.cs
new file mode 100644
index 0000000..40dea22
--- /dev/null
+++ b/src/Downkyi/Services/DictionaryResource.cs
@@ -0,0 +1,147 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Downkyi.UI.Services;
+using System;
+using System.Collections.Generic;
+
+namespace Downkyi.Services;
+
+public class DictionaryResource : IDictionaryResource
+{
+ public string GetColor(string resourceKey)
+ {
+ object? value = GetResource(Application.Current!.Resources, resourceKey);
+ return value == null ? "#00000000" : ((Color)value).ToString();
+ }
+
+ public string GetString(string resourceKey)
+ {
+ object? value = GetResource(Application.Current!.Resources, resourceKey);
+ return value == null ? "" : (string)value;
+ }
+
+ public void LoadLanguage(string languageCode)
+ {
+ LoadXamlDictionary(languageCode, "Languages");
+ }
+
+ public void LoadTheme(string theme)
+ {
+ LoadXamlDictionary($"{theme}/Theme", "Themes");
+ LoadXamlStyle($"{theme}/Styles", "Themes");
+ }
+
+ ///
+ /// 递归遍历所有资源
+ ///
+ ///
+ ///
+ ///
+ public static object? GetResource(IResourceDictionary? dictionary, string value)
+ {
+ if (dictionary == null)
+ return null;
+
+ object? result = null;
+ if (dictionary.ContainsKey(value))
+ {
+ result = dictionary[value];
+ return result;
+ }
+
+ foreach (var provider in dictionary.MergedDictionaries)
+ {
+ if (provider is ResourceDictionary resources)
+ {
+ object? temp = GetResource(resources, value);
+ if (temp != null)
+ {
+ result = temp;
+ }
+ }
+ if (provider is ResourceInclude include)
+ {
+ object? temp = GetResource(include.Loaded, value);
+ if (temp != null)
+ {
+ result = temp;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// 更换xaml资源字典
+ ///
+ /// xaml文件名,不包含扩展名
+ /// 文件所在包(路径)
+ private static void LoadXamlDictionary(string fileName, string package)
+ {
+ if (string.IsNullOrEmpty(fileName) == false)
+ {
+ var dictionariesToRemove = new List();
+
+ foreach (var dictionary in Application.Current!.Resources.MergedDictionaries)
+ {
+ if (dictionary.ToString()!.Contains("ResourceInclude"))
+ {
+ dictionariesToRemove.Add(dictionary);
+ }
+ }
+
+ foreach (var item in dictionariesToRemove)
+ {
+ Application.Current.Resources.MergedDictionaries.Remove(item);
+ }
+
+ //string? assemblyName = Assembly.GetEntryAssembly()!.GetName().Name;
+ //var xaml = new Uri($"avares://{assemblyName}/{package}/{fileName}.axaml");
+ // 直接指定程序集名称,为了预览是可以正确找到资源
+ var xaml = new Uri($"avares://Downkyi/{package}/{fileName}.axaml");
+
+ var languageDictionary = (ResourceDictionary)AvaloniaXamlLoader.Load(xaml);
+ Application.Current.Resources.MergedDictionaries.Add(languageDictionary);
+ }
+ }
+
+ ///
+ /// 更换xaml样式
+ ///
+ /// xaml文件名,不包含扩展名
+ /// 文件所在包(路径)
+ private static void LoadXamlStyle(string fileName, string package)
+ {
+ if (string.IsNullOrEmpty(fileName) == false)
+ {
+ var stylesToRemove = new List();
+
+ foreach (var style in Application.Current!.Styles)
+ {
+ if (style.ToString()!.Contains("StyleInclude"))
+ {
+ stylesToRemove.Add(style);
+ }
+ }
+
+ foreach (var item in stylesToRemove)
+ {
+ Application.Current.Styles.Remove(item);
+ }
+
+ //string? assemblyName = Assembly.GetEntryAssembly()!.GetName().Name;
+ //var xaml = new Uri($"avares://{assemblyName}/{package}/{fileName}.axaml");
+ // 直接指定程序集名称,为了预览是可以正确找到资源
+ var xaml = new Uri($"avares://Downkyi/{package}/{fileName}.axaml");
+
+ var styles = (Styles)AvaloniaXamlLoader.Load(xaml);
+ Application.Current.Styles.Add(styles);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/Services/NavigationService.cs b/src/Downkyi/Services/NavigationService.cs
new file mode 100644
index 0000000..8e475af
--- /dev/null
+++ b/src/Downkyi/Services/NavigationService.cs
@@ -0,0 +1,71 @@
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
+using Downkyi.UI.Services;
+using Downkyi.ViewModels;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Downkyi.Services;
+
+public class NavigationService : INavigationService
+{
+ public NavigationService()
+ {
+ }
+
+ public async Task ForwardAsync(string viewKey)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.Forward(viewKey);
+ }
+ }
+ });
+ }
+
+ public async Task ForwardAsync(string viewKey, Dictionary parameter)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.Forward(viewKey, parameter);
+ }
+ }
+ });
+ }
+
+ public async Task BackwardAsync()
+ {
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.Backward();
+ }
+ }
+ });
+ }
+
+ public async Task BackwardAsync(Dictionary parameter)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.Backward(parameter);
+ }
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Services/StoragePicker.cs b/src/Downkyi/Services/StoragePicker.cs
new file mode 100644
index 0000000..09ccdd8
--- /dev/null
+++ b/src/Downkyi/Services/StoragePicker.cs
@@ -0,0 +1,102 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Downkyi.UI.Services;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Web;
+
+namespace Downkyi.Services;
+
+public class StoragePicker : IStoragePicker
+{
+ public async Task FolderPicker(string directory = "")
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow?.StorageProvider is not { } provider)
+ throw new NullReferenceException("Missing StorageProvider instance.");
+
+ var storageProvider = desktop.MainWindow!.StorageProvider;
+ FolderPickerOpenOptions options = new()
+ {
+ AllowMultiple = false,
+ Title = Ioc.Default.GetRequiredService().GetString("SelectDirectory"),
+ SuggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(directory)
+ };
+
+ var folder = await storageProvider.OpenFolderPickerAsync(options);
+ if (folder.Count > 0)
+ {
+ return folder[0].Path.AbsolutePath;
+ }
+
+ return string.Empty;
+ }
+
+ public async Task SelectVideoFileAsync()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow?.StorageProvider is not { } provider)
+ throw new NullReferenceException("Missing StorageProvider instance.");
+
+ // Get top level from the current control. Alternatively, you can use Window reference instead.
+ var topLevel = TopLevel.GetTopLevel(desktop.MainWindow);
+
+ // Start async operation to open the dialog.
+ var file = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = Ioc.Default.GetRequiredService().GetString("SelectVideoFile"),
+ AllowMultiple = false,
+ FileTypeFilter = new List
+ {
+ new FilePickerFileType("mp4")
+ {
+ Patterns = new List() { "*.mp4" }
+ }
+ }
+ });
+
+ if (file.Count > 0)
+ {
+ return HttpUtility.UrlDecode(file[0].Path.AbsolutePath);
+ }
+
+ return string.Empty;
+ }
+
+ public async Task> SelectMultiVideoFileAsync()
+ {
+ if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
+ desktop.MainWindow?.StorageProvider is not { } provider)
+ throw new NullReferenceException("Missing StorageProvider instance.");
+
+ // Get top level from the current control. Alternatively, you can use Window reference instead.
+ var topLevel = TopLevel.GetTopLevel(desktop.MainWindow);
+
+ // Start async operation to open the dialog.
+ var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = Ioc.Default.GetRequiredService().GetString("SelectVideoFile"),
+ AllowMultiple = true,
+ FileTypeFilter = new List
+ {
+ new FilePickerFileType("mp4")
+ {
+ Patterns = new List() { "*.mp4" }
+ }
+ }
+ });
+
+ var result = new List();
+ foreach (var file in files)
+ {
+ result.Add(HttpUtility.UrlDecode(file.Path.AbsolutePath));
+ }
+
+ return result;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/Themes/Default/Color.axaml b/src/Downkyi/Themes/Default/Color.axaml
new file mode 100644
index 0000000..0d40d6d
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Color.axaml
@@ -0,0 +1,60 @@
+
+
+ Black
+ Black
+ white
+
+ white
+ black
+ #FF6ECEF8
+ #FF30B8F6
+ #FFE81123
+ #FFF1707A
+
+ #FFFF4D3C
+
+ #FF00A1D6
+ #C800A1D6
+ #7F00A1D6
+ #3300A1D6
+
+ #FFFB7299
+
+ #FF00A1D6
+ #FFFFFFFF
+ #FFFFFFFF
+ #FF00A1D6
+ #FFBDBDBD
+ #FFE4E4E4
+ #FFBDBDBD
+ #C8BDBDBD
+ #7FBDBDBD
+ #33BDBDBD
+
+ #7F000000
+ #33999999
+
+ #FFF4F4F4
+
+ #FFF4F4F4
+
+ #FF999999
+ #7F999999
+ #99000000
+
+ #7FD0D0D0
+ #7FFFFFFF
+ #7FD0D0D0
+
+ #FFE4E4E4
+ #FFF3CB85
+
+ white
+ black
+ #FF999999
+ #FF757575
+
+ #FFFFAE00
+ #FF02B5DA
+
+
diff --git a/src/Downkyi/Themes/Default/ColorBrush.axaml b/src/Downkyi/Themes/Default/ColorBrush.axaml
new file mode 100644
index 0000000..a69d0bd
--- /dev/null
+++ b/src/Downkyi/Themes/Default/ColorBrush.axaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles.axaml b/src/Downkyi/Themes/Default/Styles.axaml
new file mode 100644
index 0000000..6e7acdf
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/Button.axaml b/src/Downkyi/Themes/Default/Styles/Button.axaml
new file mode 100644
index 0000000..7abb96e
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/Button.axaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/CheckBox.axaml b/src/Downkyi/Themes/Default/Styles/CheckBox.axaml
new file mode 100644
index 0000000..8fb340c
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/CheckBox.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/ComboBox.axaml b/src/Downkyi/Themes/Default/Styles/ComboBox.axaml
new file mode 100644
index 0000000..d9e6cf2
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/ComboBox.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/DataGrid.axaml b/src/Downkyi/Themes/Default/Styles/DataGrid.axaml
new file mode 100644
index 0000000..6b0730f
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/DataGrid.axaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/ItemsControl.axaml b/src/Downkyi/Themes/Default/Styles/ItemsControl.axaml
new file mode 100644
index 0000000..ade2375
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/ItemsControl.axaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/ItemsRepeater.axaml b/src/Downkyi/Themes/Default/Styles/ItemsRepeater.axaml
new file mode 100644
index 0000000..604aae0
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/ItemsRepeater.axaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/ListBox.axaml b/src/Downkyi/Themes/Default/Styles/ListBox.axaml
new file mode 100644
index 0000000..35ddd71
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/ListBox.axaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/RadioButton.axaml b/src/Downkyi/Themes/Default/Styles/RadioButton.axaml
new file mode 100644
index 0000000..3eec13d
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/RadioButton.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Styles/TextBox.axaml b/src/Downkyi/Themes/Default/Styles/TextBox.axaml
new file mode 100644
index 0000000..cae2c41
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Styles/TextBox.axaml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Themes/Default/Theme.axaml b/src/Downkyi/Themes/Default/Theme.axaml
new file mode 100644
index 0000000..9dd0b58
--- /dev/null
+++ b/src/Downkyi/Themes/Default/Theme.axaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/ViewLocator.cs b/src/Downkyi/ViewLocator.cs
new file mode 100644
index 0000000..317b05c
--- /dev/null
+++ b/src/Downkyi/ViewLocator.cs
@@ -0,0 +1,37 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using Downkyi.UI.Mvvm;
+using System;
+
+namespace Downkyi;
+
+public class ViewLocator : IDataTemplate
+{
+ public Control? Build(object? data)
+ {
+ string name = data!.GetType().FullName!.Replace("ViewModel", "View");
+
+ if (name.Contains(".UI"))
+ {
+ name = name.Replace(".UI", "");
+ }
+ if (name.Contains("Proxy"))
+ {
+ name = name.Replace("Proxy", "");
+ }
+
+ var type = Type.GetType(name);
+
+ if (type != null)
+ {
+ return (Control)Activator.CreateInstance(type)!;
+ }
+
+ return new TextBlock { Text = "Not Found: " + name };
+ }
+
+ public bool Match(object? data)
+ {
+ return data is ViewModelBase;
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/Login/QRCodeViewModelProxy.cs b/src/Downkyi/ViewModels/Login/QRCodeViewModelProxy.cs
new file mode 100644
index 0000000..5db5cb7
--- /dev/null
+++ b/src/Downkyi/ViewModels/Login/QRCodeViewModelProxy.cs
@@ -0,0 +1,52 @@
+using Avalonia.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.Login;
+using QRCoder;
+using System.IO;
+
+namespace Downkyi.ViewModels.Login;
+
+public partial class QRCodeViewModelProxy : QRCodeViewModel
+{
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private Bitmap? _QRCode;
+
+ #endregion
+
+ public QRCodeViewModelProxy(BaseServices baseServices) : base(baseServices)
+ {
+ // 根据请求到的url生成二维码
+ _QRCodeAction = new((url) =>
+ {
+ byte[]? qrcodeByte = GenerateQrCode(url, 10);
+ using var ms = new MemoryStream(qrcodeByte);
+ Bitmap img = new(ms);
+ QRCode = img;
+ });
+ }
+
+ protected override void InitStatus()
+ {
+ base.InitStatus();
+
+ QRCode = null;
+ }
+
+ ///
+ /// 生成二维码
+ ///
+ /// 信息
+ /// 版本 1 ~ 40
+ private static byte[] GenerateQrCode(string msg, int version)
+ {
+ QRCodeGenerator qrGenerator = new();
+ QRCodeData qrCodeData = qrGenerator.CreateQrCode(msg, QRCodeGenerator.ECCLevel.H/* 这里设置容错率的一个级别 */, true, false, QRCodeGenerator.EciMode.Utf8, version);
+ BitmapByteQRCode qrCode = new(qrCodeData);
+ var codeByte = qrCode.GetGraphic(20);
+ return codeByte;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/MainWindowViewModel.cs b/src/Downkyi/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..ed48015
--- /dev/null
+++ b/src/Downkyi/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,115 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Downkyi.Core.Utils;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels;
+using Downkyi.UI.ViewModels.DownloadManager;
+using Downkyi.UI.ViewModels.Login;
+using Downkyi.UI.ViewModels.Settings;
+using Downkyi.UI.ViewModels.Toolbox;
+using Downkyi.UI.ViewModels.User;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Downkyi.ViewModels;
+
+public partial class MainWindowViewModel : ViewModelBase
+{
+ public const string Key = "Main";
+
+ private readonly LinkStack _pages = new();
+
+ #region 页面属性申明
+
+ [ObservableProperty]
+ private ViewModelBase? _content = null;
+
+ [ObservableProperty]
+ private bool _messageVisibility = false;
+
+ [ObservableProperty]
+ private string _message = string.Empty;
+
+ #endregion
+
+ public MainWindowViewModel(BaseServices baseServices) : base(baseServices)
+ {
+ Dictionary parameter = new()
+ {
+ { "key", Key },
+ { "result", "start" },
+ };
+ Forward(IndexViewModel.Key, parameter);
+
+ // 订阅消息发送事件
+ string oldMessage;
+ NotificationEvent.Subscribe((message) =>
+ {
+ oldMessage = Message;
+ Message = message;
+ int sleep = 2000;
+ if (oldMessage == Message) { sleep = 1500; }
+
+ MessageVisibility = true;
+ Thread.Sleep(sleep);
+ MessageVisibility = false;
+ });
+
+ }
+
+ public void Forward(string viewKey, Dictionary? parameter = null)
+ {
+ var viewModel = SetContent(viewKey);
+ if (viewModel == null) { return; }
+
+ viewModel.OnNavigatedTo(parameter);
+ Content = viewModel;
+
+ _pages.Push(viewKey);
+ }
+
+ public void Backward(Dictionary? parameter = null)
+ {
+ _pages.Pop();
+ var viewModel = SetContent(_pages.Peek());
+ if (viewModel == null) { return; }
+
+ viewModel.OnNavigatedTo(parameter);
+ Content = viewModel;
+ }
+
+ private static ViewModelBase? SetContent(string? viewKey)
+ {
+ if (viewKey == null) { return null; }
+
+ ViewModelBase? viewModel = null;
+ switch (viewKey)
+ {
+ case IndexViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ case LoginViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ case MySpaceViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ case UserSpaceViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ case SettingsViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ case DownloadManagerViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ case ToolboxViewModel.Key:
+ viewModel = Ioc.Default.GetRequiredService();
+ break;
+ default:
+ break;
+ }
+ return viewModel;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/Settings/AboutViewModelProxy.cs b/src/Downkyi/ViewModels/Settings/AboutViewModelProxy.cs
new file mode 100644
index 0000000..8ad0925
--- /dev/null
+++ b/src/Downkyi/ViewModels/Settings/AboutViewModelProxy.cs
@@ -0,0 +1,112 @@
+using Downkyi.Models;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.Settings;
+using System.Threading.Tasks;
+
+namespace Downkyi.ViewModels.Settings;
+
+public class AboutViewModelProxy : AboutViewModel
+{
+ public AboutViewModelProxy(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+
+ AppInfo app = new();
+ AppName = app.Name;
+ AppVersion = app.VersionName;
+
+ AddThirdParty();
+
+ #endregion
+ }
+
+ private async void AddThirdParty()
+ {
+ await Task.Run(() =>
+ {
+ ThirdParties.Add(new()
+ {
+ Name = "Aria2cNet",
+ Author = "Leiurayer",
+ Version = "1.0.1",
+ License = "MIT",
+ Homepage = "https://github.com/leiurayer/Aria2cNet",
+ LicenseUrl = "https://licenses.nuget.org/MIT"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "AsyncImageLoader.Avalonia",
+ Author = "SKProCH",
+ Version = "3.0.0-avalonia11-preview4",
+ License = "LICENSE",
+ Homepage = "https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia",
+ LicenseUrl = "https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/LICENSE"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "Avalonia",
+ Author = "Avalonia Team",
+ Version = "11.0.0",
+ License = "MIT",
+ Homepage = "https://avaloniaui.net/",
+ LicenseUrl = "https://licenses.nuget.org/MIT"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "CommunityToolkit.Mvvm",
+ Author = "Microsoft",
+ Version = "8.2.1",
+ License = "MIT",
+ Homepage = "https://github.com/CommunityToolkit/dotnet",
+ LicenseUrl = "https://licenses.nuget.org/MIT"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "MessageBox.Avalonia",
+ Author = "Lary",
+ Version = "3.0.0",
+ License = "MIT",
+ Homepage = "https://github.com/CreateLab/MessageBox.Avalonia",
+ LicenseUrl = "https://licenses.nuget.org/MIT"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "Newtonsoft.Json",
+ Author = "James Newton-King",
+ Version = "13.0.3",
+ License = "MIT",
+ Homepage = "https://www.newtonsoft.com/json",
+ LicenseUrl = "https://licenses.nuget.org/MIT"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "NLog",
+ Author = "Jarek Kowalski et al.",
+ Version = "5.2.2",
+ License = "BSD-3-Clause",
+ Homepage = "https://nlog-project.org/",
+ LicenseUrl = "https://licenses.nuget.org/BSD-3-Clause"
+ });
+ ThirdParties.Add(new()
+ {
+ Name = "QRCoder",
+ Author = "Raffael Herrmann",
+ Version = "1.4.3",
+ License = "MIT",
+ Homepage = "https://github.com/codebude/QRCoder/",
+ LicenseUrl = "https://licenses.nuget.org/MIT"
+ });
+
+ ThirdParties.Add(new()
+ {
+ Name = "sqlite-net-pcl",
+ Author = "SQLite-net",
+ Version = "1.8.116",
+ License = "LICENSE",
+ Homepage = "https://github.com/praeclarum/sqlite-net",
+ LicenseUrl = "https://www.nuget.org/packages/sqlite-net-pcl/1.8.116/license"
+ });
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/Settings/DanmakuViewModelProxy.cs b/src/Downkyi/ViewModels/Settings/DanmakuViewModelProxy.cs
new file mode 100644
index 0000000..b86cbd0
--- /dev/null
+++ b/src/Downkyi/ViewModels/Settings/DanmakuViewModelProxy.cs
@@ -0,0 +1,21 @@
+using Avalonia.Media;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.Settings;
+
+namespace Downkyi.ViewModels.Settings;
+
+public class DanmakuViewModelProxy : DanmakuViewModel
+{
+ public DanmakuViewModelProxy(BaseServices baseServices) : base(baseServices)
+ {
+ #region 属性初始化
+
+ var fonts = FontManager.Current.SystemFonts;
+ foreach (var font in fonts)
+ {
+ Fonts.Add(font.Name);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/Settings/VideoViewModelProxy.cs b/src/Downkyi/ViewModels/Settings/VideoViewModelProxy.cs
new file mode 100644
index 0000000..33d33ec
--- /dev/null
+++ b/src/Downkyi/ViewModels/Settings/VideoViewModelProxy.cs
@@ -0,0 +1,128 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.Core.FileName;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.Settings;
+
+namespace Downkyi.ViewModels.Settings;
+
+public partial class VideoViewModelProxy : VideoViewModel
+{
+ public VideoViewModelProxy(BaseServices baseServices) : base(baseServices)
+ {
+ }
+
+ #region 命令申明
+
+ ///
+ /// 可选文件名字段点击事件
+ ///
+ ///
+ [RelayCommand]
+ private void SelectOptionalFields(object args)
+ {
+ if (args is PointerReleasedEventArgs releasedEventArgs)
+ {
+ if (releasedEventArgs.InitialPressMouseButton == MouseButton.Left)
+ {
+ if (releasedEventArgs.Source is Border border)
+ {
+ OptionalFieldsCommand(border.Child?.DataContext);
+ }
+ if (releasedEventArgs.Source is TextBlock text)
+ {
+ OptionalFieldsCommand(text.DataContext);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 选中文件名字段右键点击事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetSelectedFileName(object args)
+ {
+ if (args is PointerReleasedEventArgs releasedEventArgs)
+ {
+ if (releasedEventArgs.InitialPressMouseButton == MouseButton.Right)
+ {
+ if (releasedEventArgs.Source is Border border)
+ {
+ SelectedFileNameRightButton(border.Child?.DataContext);
+ }
+ if (releasedEventArgs.Source is TextBlock text)
+ {
+ SelectedFileNameRightButton(text.DataContext);
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ protected override string DisplayFileNamePart(FileNamePart item)
+ {
+ string display = string.Empty;
+ switch (item)
+ {
+ case FileNamePart.ORDER:
+ display = DictionaryResource.GetString("DisplayOrder");
+ break;
+ case FileNamePart.SECTION:
+ display = DictionaryResource.GetString("DisplaySection");
+ break;
+ case FileNamePart.MAIN_TITLE:
+ display = DictionaryResource.GetString("DisplayMainTitle");
+ break;
+ case FileNamePart.PAGE_TITLE:
+ display = DictionaryResource.GetString("DisplayPageTitle");
+ break;
+ case FileNamePart.VIDEO_ZONE:
+ display = DictionaryResource.GetString("DisplayVideoZone");
+ break;
+ case FileNamePart.AUDIO_QUALITY:
+ display = DictionaryResource.GetString("DisplayAudioQuality");
+ break;
+ case FileNamePart.VIDEO_QUALITY:
+ display = DictionaryResource.GetString("DisplayVideoQuality");
+ break;
+ case FileNamePart.VIDEO_CODEC:
+ display = DictionaryResource.GetString("DisplayVideoCodec");
+ break;
+ case FileNamePart.VIDEO_PUBLISH_TIME:
+ display = DictionaryResource.GetString("DisplayVideoPublishTime");
+ break;
+ case FileNamePart.AVID:
+ display = "avid";
+ break;
+ case FileNamePart.BVID:
+ display = "bvid";
+ break;
+ case FileNamePart.CID:
+ display = "cid";
+ break;
+ case FileNamePart.UP_MID:
+ display = DictionaryResource.GetString("DisplayUpMid");
+ break;
+ case FileNamePart.UP_NAME:
+ display = DictionaryResource.GetString("DisplayUpName");
+ break;
+ }
+
+ if (((int)item) >= 100)
+ {
+ display = HyphenSeparated.Hyphen[(int)item];
+ }
+
+ if (display == " ")
+ {
+ display = DictionaryResource.GetString("DisplaySpace");
+ }
+
+ return display;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/Toolbox/DelogoViewModelProxy.cs b/src/Downkyi/ViewModels/Toolbox/DelogoViewModelProxy.cs
new file mode 100644
index 0000000..efe75cc
--- /dev/null
+++ b/src/Downkyi/ViewModels/Toolbox/DelogoViewModelProxy.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.Toolbox;
+
+namespace Downkyi.ViewModels.Toolbox;
+
+public partial class DelogoViewModelProxy : DelogoViewModel
+{
+ public DelogoViewModelProxy(BaseServices baseServices) : base(baseServices)
+ {
+ }
+
+ #region 命令申明
+
+ ///
+ /// Status改变事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetStatus(object parameter)
+ {
+ if (parameter is not TextBox output) { return; }
+
+ // TextBox滚动到底部
+ output.SelectionStart = Status.Length;
+ output.SelectionEnd = Status.Length;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Downkyi/ViewModels/Toolbox/ExtractMediaViewModelProxy.cs b/src/Downkyi/ViewModels/Toolbox/ExtractMediaViewModelProxy.cs
new file mode 100644
index 0000000..329a27c
--- /dev/null
+++ b/src/Downkyi/ViewModels/Toolbox/ExtractMediaViewModelProxy.cs
@@ -0,0 +1,31 @@
+using Avalonia.Controls;
+using CommunityToolkit.Mvvm.Input;
+using Downkyi.UI.Mvvm;
+using Downkyi.UI.ViewModels.Toolbox;
+
+namespace Downkyi.ViewModels.Toolbox;
+
+public partial class ExtractMediaViewModelProxy : ExtractMediaViewModel
+{
+ public ExtractMediaViewModelProxy(BaseServices baseServices) : base(baseServices)
+ {
+ }
+
+ #region 命令申明
+
+ ///
+ /// Status改变事件
+ ///
+ ///
+ [RelayCommand]
+ private void SetStatus(object parameter)
+ {
+ if (parameter is not TextBox output) { return; }
+
+ // TextBox滚动到底部
+ output.SelectionStart = Status.Length;
+ output.SelectionEnd = Status.Length;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml
new file mode 100644
index 0000000..837a983
--- /dev/null
+++ b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml
@@ -0,0 +1,14 @@
+
+ Welcome to DownloadFinishedView!
+
diff --git a/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml.cs b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml.cs
new file mode 100644
index 0000000..7130f9d
--- /dev/null
+++ b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.DownloadManager
+{
+ public partial class DownloadFinishedView : UserControl
+ {
+ public DownloadFinishedView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml
new file mode 100644
index 0000000..996c28b
--- /dev/null
+++ b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml.cs b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml.cs
new file mode 100644
index 0000000..817db8e
--- /dev/null
+++ b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.DownloadManager
+{
+ public partial class DownloadManagerView : UserControl
+ {
+ public DownloadManagerView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/DownloadManager/DownloadingView.axaml b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml
new file mode 100644
index 0000000..7152cb4
--- /dev/null
+++ b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml
@@ -0,0 +1,14 @@
+
+ Welcome to DownloadingView!
+
diff --git a/src/Downkyi/Views/DownloadManager/DownloadingView.axaml.cs b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml.cs
new file mode 100644
index 0000000..383ae0a
--- /dev/null
+++ b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.DownloadManager
+{
+ public partial class DownloadingView : UserControl
+ {
+ public DownloadingView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/IndexView.axaml b/src/Downkyi/Views/IndexView.axaml
new file mode 100644
index 0000000..72f0200
--- /dev/null
+++ b/src/Downkyi/Views/IndexView.axaml
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Downkyi/Views/IndexView.axaml.cs b/src/Downkyi/Views/IndexView.axaml.cs
new file mode 100644
index 0000000..a98581e
--- /dev/null
+++ b/src/Downkyi/Views/IndexView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views
+{
+ public partial class IndexView : UserControl
+ {
+ public IndexView()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Views/Login/CookiesView.axaml b/src/Downkyi/Views/Login/CookiesView.axaml
new file mode 100644
index 0000000..c957bbd
--- /dev/null
+++ b/src/Downkyi/Views/Login/CookiesView.axaml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Login/CookiesView.axaml.cs b/src/Downkyi/Views/Login/CookiesView.axaml.cs
new file mode 100644
index 0000000..2b5678a
--- /dev/null
+++ b/src/Downkyi/Views/Login/CookiesView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Login
+{
+ public partial class CookiesView : UserControl
+ {
+ public CookiesView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Login/LoginView.axaml b/src/Downkyi/Views/Login/LoginView.axaml
new file mode 100644
index 0000000..2be945b
--- /dev/null
+++ b/src/Downkyi/Views/Login/LoginView.axaml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Login/LoginView.axaml.cs b/src/Downkyi/Views/Login/LoginView.axaml.cs
new file mode 100644
index 0000000..430614c
--- /dev/null
+++ b/src/Downkyi/Views/Login/LoginView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Login
+{
+ public partial class LoginView : UserControl
+ {
+ public LoginView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Login/QRCodeView.axaml b/src/Downkyi/Views/Login/QRCodeView.axaml
new file mode 100644
index 0000000..0f0cde6
--- /dev/null
+++ b/src/Downkyi/Views/Login/QRCodeView.axaml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Login/QRCodeView.axaml.cs b/src/Downkyi/Views/Login/QRCodeView.axaml.cs
new file mode 100644
index 0000000..a6bc04f
--- /dev/null
+++ b/src/Downkyi/Views/Login/QRCodeView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Login
+{
+ public partial class QRCodeView : UserControl
+ {
+ public QRCodeView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/MainWindow.axaml b/src/Downkyi/Views/MainWindow.axaml
new file mode 100644
index 0000000..e7f0b1a
--- /dev/null
+++ b/src/Downkyi/Views/MainWindow.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Downkyi/Views/MainWindow.axaml.cs b/src/Downkyi/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..327d22e
--- /dev/null
+++ b/src/Downkyi/Views/MainWindow.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views
+{
+ public partial class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Downkyi/Views/Settings/AboutView.axaml b/src/Downkyi/Views/Settings/AboutView.axaml
new file mode 100644
index 0000000..f936422
--- /dev/null
+++ b/src/Downkyi/Views/Settings/AboutView.axaml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Settings/AboutView.axaml.cs b/src/Downkyi/Views/Settings/AboutView.axaml.cs
new file mode 100644
index 0000000..ccfb32f
--- /dev/null
+++ b/src/Downkyi/Views/Settings/AboutView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Settings
+{
+ public partial class AboutView : UserControl
+ {
+ public AboutView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Settings/BasicView.axaml b/src/Downkyi/Views/Settings/BasicView.axaml
new file mode 100644
index 0000000..629ea38
--- /dev/null
+++ b/src/Downkyi/Views/Settings/BasicView.axaml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Settings/BasicView.axaml.cs b/src/Downkyi/Views/Settings/BasicView.axaml.cs
new file mode 100644
index 0000000..7bb8016
--- /dev/null
+++ b/src/Downkyi/Views/Settings/BasicView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Settings
+{
+ public partial class BasicView : UserControl
+ {
+ public BasicView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Settings/DanmakuView.axaml b/src/Downkyi/Views/Settings/DanmakuView.axaml
new file mode 100644
index 0000000..509d1f5
--- /dev/null
+++ b/src/Downkyi/Views/Settings/DanmakuView.axaml
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Settings/DanmakuView.axaml.cs b/src/Downkyi/Views/Settings/DanmakuView.axaml.cs
new file mode 100644
index 0000000..dfea62f
--- /dev/null
+++ b/src/Downkyi/Views/Settings/DanmakuView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Settings
+{
+ public partial class DanmakuView : UserControl
+ {
+ public DanmakuView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Settings/NetworkView.axaml b/src/Downkyi/Views/Settings/NetworkView.axaml
new file mode 100644
index 0000000..8be9eb9
--- /dev/null
+++ b/src/Downkyi/Views/Settings/NetworkView.axaml
@@ -0,0 +1,474 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Settings/NetworkView.axaml.cs b/src/Downkyi/Views/Settings/NetworkView.axaml.cs
new file mode 100644
index 0000000..5d774cf
--- /dev/null
+++ b/src/Downkyi/Views/Settings/NetworkView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Settings
+{
+ public partial class NetworkView : UserControl
+ {
+ public NetworkView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Settings/SettingsView.axaml b/src/Downkyi/Views/Settings/SettingsView.axaml
new file mode 100644
index 0000000..50e2989
--- /dev/null
+++ b/src/Downkyi/Views/Settings/SettingsView.axaml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Settings/SettingsView.axaml.cs b/src/Downkyi/Views/Settings/SettingsView.axaml.cs
new file mode 100644
index 0000000..d5c8a0e
--- /dev/null
+++ b/src/Downkyi/Views/Settings/SettingsView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Settings
+{
+ public partial class SettingsView : UserControl
+ {
+ public SettingsView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Settings/VideoView.axaml b/src/Downkyi/Views/Settings/VideoView.axaml
new file mode 100644
index 0000000..59da197
--- /dev/null
+++ b/src/Downkyi/Views/Settings/VideoView.axaml
@@ -0,0 +1,342 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Settings/VideoView.axaml.cs b/src/Downkyi/Views/Settings/VideoView.axaml.cs
new file mode 100644
index 0000000..112765c
--- /dev/null
+++ b/src/Downkyi/Views/Settings/VideoView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Settings
+{
+ public partial class VideoView : UserControl
+ {
+ public VideoView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/SplashWindow.axaml b/src/Downkyi/Views/SplashWindow.axaml
new file mode 100644
index 0000000..fdb9940
--- /dev/null
+++ b/src/Downkyi/Views/SplashWindow.axaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/SplashWindow.axaml.cs b/src/Downkyi/Views/SplashWindow.axaml.cs
new file mode 100644
index 0000000..bc4eb2a
--- /dev/null
+++ b/src/Downkyi/Views/SplashWindow.axaml.cs
@@ -0,0 +1,43 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Downkyi.Models;
+using System;
+
+namespace Downkyi.Views;
+
+public partial class SplashWindow : Window
+{
+ private readonly Action? _mainAction;
+
+ public SplashWindow()
+ {
+ }
+
+ public SplashWindow(Action mainAction)
+ {
+ InitializeComponent();
+
+ nameVersion.Text = new AppInfo().VersionName;
+ _mainAction = mainAction;
+ }
+
+ protected override void OnLoaded(RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+ DummyLoad();
+ }
+
+ private async void DummyLoad()
+ {
+ // Do some background stuff here.
+ //Task.Delay(1000);
+
+ await Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ _mainAction?.Invoke();
+ Close();
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/src/Downkyi/Views/Toolbox/BiliHelperView.axaml b/src/Downkyi/Views/Toolbox/BiliHelperView.axaml
new file mode 100644
index 0000000..31f1887
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/BiliHelperView.axaml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Toolbox/BiliHelperView.axaml.cs b/src/Downkyi/Views/Toolbox/BiliHelperView.axaml.cs
new file mode 100644
index 0000000..e2d151c
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/BiliHelperView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Toolbox
+{
+ public partial class BiliHelperView : UserControl
+ {
+ public BiliHelperView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Toolbox/DelogoView.axaml b/src/Downkyi/Views/Toolbox/DelogoView.axaml
new file mode 100644
index 0000000..95dd491
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/DelogoView.axaml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Toolbox/DelogoView.axaml.cs b/src/Downkyi/Views/Toolbox/DelogoView.axaml.cs
new file mode 100644
index 0000000..67d1b86
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/DelogoView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Toolbox
+{
+ public partial class DelogoView : UserControl
+ {
+ public DelogoView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Toolbox/ExtractMediaView.axaml b/src/Downkyi/Views/Toolbox/ExtractMediaView.axaml
new file mode 100644
index 0000000..d872d4b
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/ExtractMediaView.axaml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Toolbox/ExtractMediaView.axaml.cs b/src/Downkyi/Views/Toolbox/ExtractMediaView.axaml.cs
new file mode 100644
index 0000000..dfbd193
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/ExtractMediaView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Toolbox
+{
+ public partial class ExtractMediaView : UserControl
+ {
+ public ExtractMediaView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/Toolbox/ToolboxView.axaml b/src/Downkyi/Views/Toolbox/ToolboxView.axaml
new file mode 100644
index 0000000..ee83350
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/ToolboxView.axaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/Toolbox/ToolboxView.axaml.cs b/src/Downkyi/Views/Toolbox/ToolboxView.axaml.cs
new file mode 100644
index 0000000..9e81914
--- /dev/null
+++ b/src/Downkyi/Views/Toolbox/ToolboxView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.Toolbox
+{
+ public partial class ToolboxView : UserControl
+ {
+ public ToolboxView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/User/MySpaceView.axaml b/src/Downkyi/Views/User/MySpaceView.axaml
new file mode 100644
index 0000000..b4cc9b4
--- /dev/null
+++ b/src/Downkyi/Views/User/MySpaceView.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/User/MySpaceView.axaml.cs b/src/Downkyi/Views/User/MySpaceView.axaml.cs
new file mode 100644
index 0000000..4f8067a
--- /dev/null
+++ b/src/Downkyi/Views/User/MySpaceView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.User
+{
+ public partial class MySpaceView : UserControl
+ {
+ public MySpaceView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/Views/User/UserSpaceView.axaml b/src/Downkyi/Views/User/UserSpaceView.axaml
new file mode 100644
index 0000000..4468f49
--- /dev/null
+++ b/src/Downkyi/Views/User/UserSpaceView.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Downkyi/Views/User/UserSpaceView.axaml.cs b/src/Downkyi/Views/User/UserSpaceView.axaml.cs
new file mode 100644
index 0000000..8e1f430
--- /dev/null
+++ b/src/Downkyi/Views/User/UserSpaceView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Downkyi.Views.User
+{
+ public partial class UserSpaceView : UserControl
+ {
+ public UserSpaceView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Downkyi/app.manifest b/src/Downkyi/app.manifest
new file mode 100644
index 0000000..e0ce8d0
--- /dev/null
+++ b/src/Downkyi/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..9b191a5
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1 @@
+# Downkyi
\ No newline at end of file
diff --git a/third_party/aria2-1.36.0-aarch64-linux-android-build1.zip b/third_party/aria2-1.36.0-aarch64-linux-android-build1.zip
new file mode 100644
index 0000000..ff916da
Binary files /dev/null and b/third_party/aria2-1.36.0-aarch64-linux-android-build1.zip differ
diff --git a/third_party/aria2-1.36.0-win-32bit-build1.zip b/third_party/aria2-1.36.0-win-32bit-build1.zip
new file mode 100644
index 0000000..99fe5c1
Binary files /dev/null and b/third_party/aria2-1.36.0-win-32bit-build1.zip differ
diff --git a/third_party/aria2-1.36.0-win-64bit-build1.zip b/third_party/aria2-1.36.0-win-64bit-build1.zip
new file mode 100644
index 0000000..260e3a4
Binary files /dev/null and b/third_party/aria2-1.36.0-win-64bit-build1.zip differ
diff --git a/third_party/aria2-1.36.0.tar.xz b/third_party/aria2-1.36.0.tar.xz
new file mode 100644
index 0000000..2f1e333
Binary files /dev/null and b/third_party/aria2-1.36.0.tar.xz differ