├── .gitignore ├── Otokoneko.Base ├── Client.cs ├── DataType │ ├── Library.cs │ ├── Manga.cs │ ├── Message.cs │ ├── Plugin.cs │ ├── ScheduleTask.cs │ ├── Search.cs │ └── User.cs ├── Message.cs ├── Model.cs ├── Network.cs ├── Otokoneko.Base.projitems ├── Otokoneko.Base.shproj ├── Server.cs ├── Utils │ ├── AsyncQueue.cs │ ├── Cache.cs │ ├── PasswordHashProvider.cs │ └── ThreadSafeList.cs └── message.proto ├── Otokoneko.Client.WPFClient ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── Model │ ├── DataType.cs │ ├── Model.cs │ └── Setting.cs ├── Otokoneko.Client.WPFClient.csproj ├── Otokoneko.Client.WPFClient.sln ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── Utils │ ├── DependencyObjectExtensions.cs │ ├── FileUtils.cs │ ├── FormatUtils.cs │ ├── ImageUtils.cs │ └── MathUtils.cs ├── View │ ├── AlwaysRefreshDataTemplateSelector.cs │ ├── Behavior.cs │ ├── CommentWindow.xaml │ ├── CommentWindow.xaml.cs │ ├── Converter.cs │ ├── CrashReporter.xaml │ ├── CrashReporter.xaml.cs │ ├── Explorer.xaml │ ├── Explorer.xaml.cs │ ├── InputForm.xaml │ ├── InputForm.xaml.cs │ ├── LibraryDetailWindow.xaml │ ├── LibraryDetailWindow.xaml.cs │ ├── LibraryManager.xaml │ ├── LibraryManager.xaml.cs │ ├── LoginWindow.xaml │ ├── LoginWindow.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── MangaDetail.xaml │ ├── MangaDetail.xaml.cs │ ├── MangaExplorer.xaml │ ├── MangaExplorer.xaml.cs │ ├── MangaReader.xaml │ ├── MangaReader.xaml.cs │ ├── MangaSearchResult.xaml │ ├── MangaSearchResult.xaml.cs │ ├── MessageBoxView.xaml │ ├── MessageBoxView.xaml.cs │ ├── PlanDetailView.xaml │ ├── PlanDetailView.xaml.cs │ ├── PlanManager.xaml │ ├── PlanManager.xaml.cs │ ├── PluginManager.xaml │ ├── PluginManager.xaml.cs │ ├── ScoreControl.xaml │ ├── ScoreControl.xaml.cs │ ├── SettingView.xaml │ ├── SettingView.xaml.cs │ ├── SmoothScrollViewer.cs │ ├── TagDetailView.xaml │ ├── TagDetailView.xaml.cs │ ├── TagManager.xaml │ ├── TagManager.xaml.cs │ ├── TagSelectionWindow.xaml │ ├── TagSelectionWindow.xaml.cs │ ├── TagTypeManager.xaml │ ├── TagTypeManager.xaml.cs │ ├── TagTypeManagerWindow.xaml │ ├── TagTypeManagerWindow.xaml.cs │ ├── TaskDetailView.xaml │ ├── TaskDetailView.xaml.cs │ ├── TaskScheduler.xaml │ ├── TaskScheduler.xaml.cs │ ├── UserManager.xaml │ └── UserManager.xaml.cs ├── ViewModel │ ├── BaseViewModel.cs │ ├── CircleButtonViewModel.cs │ ├── CommentViewModel.cs │ ├── DisplayImage.cs │ ├── DisplayManga.cs │ ├── DisplayTag.cs │ ├── DisplayTagType.cs │ ├── ExplorerHeader.cs │ ├── ExplorerViewModel.cs │ ├── LibraryDetailViewModel.cs │ ├── LibraryManagerViewModel.cs │ ├── LoginViewModel.cs │ ├── MainViewModel.cs │ ├── MangaDetailViewModel.cs │ ├── MangaExplorerViewModel.cs │ ├── MangaReaderViewModel.cs │ ├── MangaSearchResultViewModel.cs │ ├── MessageBoxViewModel.cs │ ├── NavigationService.cs │ ├── PlanDetailViewModel.cs │ ├── PlanExplorerViewModel.cs │ ├── PlanManagerViewModel.cs │ ├── PluginManagerViewModel.cs │ ├── SearchServiceViewModel.cs │ ├── SettingViewModel.cs │ ├── TagDetailViewModel.cs │ ├── TagExplorerViewModel.cs │ ├── TagManagerViewModel.cs │ ├── TagSelectionViewModel.cs │ ├── TagTypeManagerViewModel.cs │ ├── TaskDetailViewModel.cs │ ├── TaskExplorerViewModel.cs │ ├── TaskSchedulerViewModel.cs │ └── UserManagerViewModel.cs ├── icon │ ├── FTP.png │ ├── books.png │ ├── cancel.png │ ├── check.png │ ├── clipboard.png │ ├── close.png │ ├── cloud.png │ ├── confirm.png │ ├── direct-download.png │ ├── edit.png │ ├── enter.png │ ├── envelope.png │ ├── favorite.png │ ├── filter.png │ ├── first.png │ ├── flash.png │ ├── folder.png │ ├── full-size.png │ ├── gear.png │ ├── grid.png │ ├── hard-disc.png │ ├── history.png │ ├── home.png │ ├── increase-size-option.png │ ├── label.png │ ├── last.png │ ├── left-arrow.png │ ├── list.png │ ├── next.png │ ├── plus.png │ ├── prev.png │ ├── refresh.png │ ├── resize-option.png │ ├── right-arrow.png │ ├── save.png │ ├── search.png │ ├── star-empty.png │ ├── star-half-empty.png │ ├── star.png │ ├── transfer.png │ ├── trash.png │ └── user.png └── lang │ └── zh_CN.xaml ├── Otokoneko.Plugins ├── Otokoneko.Plugins.Base │ ├── Handler │ │ ├── RateLimiterHandler.cs │ │ ├── RetryHandler.cs │ │ └── TimeoutHandler.cs │ ├── Otokoneko.Plugins.Base.projitems │ └── Otokoneko.Plugins.Base.shproj ├── Otokoneko.Plugins.CopyManga │ ├── CopyMangaDownloader.cs │ ├── CopyMangaScraper.cs │ └── Otokoneko.Plugins.CopyManga.csproj ├── Otokoneko.Plugins.Dmzj │ ├── DmzjDownloader.cs │ ├── DmzjDownloaderScraper.cs │ ├── DmzjMessage.cs │ ├── Otokoneko.Plugins.Dmzj.csproj │ └── dmzj.message.proto ├── Otokoneko.Plugins.Interface │ ├── IMangaDownloader.cs │ ├── IMetadataScraper.cs │ ├── IPlugin.cs │ ├── Otokoneko.Plugins.Interface.csproj │ └── RequiredParameterAttribute.cs ├── Otokoneko.Plugins.Manhuagui │ ├── ManhuaguiDownloader.cs │ ├── ManhuaguiScraper.cs │ └── Otokoneko.Plugins.Manhuagui.csproj ├── Otokoneko.Plugins.NHentai │ ├── NHentai.cs │ └── Otokoneko.Plugins.NHentai.csproj └── Otokoneko.Plugins.sln ├── Otokoneko.Server.Gui ├── ConsoleDisplayer.Designer.cs ├── ConsoleDisplayer.cs ├── ConsoleDisplayer.resx ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── Otokoneko.Server.Gui.csproj ├── ProcessInterface.cs ├── Program.cs ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml └── Utils │ └── AnsiEscapeCodeUtils.cs ├── Otokoneko.Server ├── Config │ ├── Config.cs │ ├── ConfigComment.cs │ └── ConfigLoader.cs ├── Converter │ ├── FileTreeNodeFormatter.cs │ └── FileTreeNodeToMangaConverter.cs ├── LibraryManage │ ├── DataProvider.cs │ ├── DataType.cs │ ├── LibraryItemHandler.cs │ └── LibraryManager.cs ├── MangaManage │ ├── DataFormatter.cs │ ├── DataProvider.cs │ ├── DataType.cs │ └── MangaManager.cs ├── MessageBox │ ├── DataProvider.cs │ └── MessageManager.cs ├── Otokoneko.Server.csproj ├── Otokoneko.Server.sln ├── PluginManage │ ├── PluginLoader.cs │ ├── PluginManager.cs │ └── PluginParameterProvider.cs ├── Program.cs ├── Properties │ └── PublishProfiles │ │ ├── linux-x64.pubxml │ │ ├── win-x86.pubxml │ │ └── win-x86_gui.pubxml ├── ScheduleTaskManage │ ├── DataType.cs │ ├── PlanHandler.cs │ ├── PlanManager.cs │ ├── ScheduleTask.cs │ ├── ScheduleTaskHandler.cs │ ├── Scheduler.cs │ └── TriggerHandler.cs ├── SearchService │ ├── FavoriteMangaSearchService.cs │ ├── FtsIndexService.cs │ ├── MangaKeywordSearchService.cs │ ├── MangaReadHistorySearchService.cs │ └── TagKeywordSearchService.cs ├── Server.cs ├── UserManage │ ├── DataProvider.cs │ └── UserManager.cs └── Utils │ ├── AnsiEscapeColorUtils.cs │ ├── ArchiveFileDataGenerator.cs │ ├── AtomicCounter.cs │ ├── CertificateUtils.cs │ ├── ConsoleColorConfig.cs │ ├── DirectoryUtils.cs │ ├── EncryptUtils.cs │ ├── ImageUtils.cs │ ├── RollingFileByMaxAgeAppender.cs │ └── ServerConfigStringGenerator.cs ├── README.md └── screenshot ├── 1.png ├── 2.png ├── 3.png └── 4.png /.gitignore: -------------------------------------------------------------------------------- 1 | /**/bin/ 2 | /**/obj/ 3 | /**/.vs/ 4 | *.user -------------------------------------------------------------------------------- /Otokoneko.Base/DataType/Library.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using MessagePack; 4 | #if SERVER 5 | #endif 6 | 7 | namespace Otokoneko.DataType 8 | { 9 | public enum FileStructType 10 | { 11 | None = 0, 12 | Image = 1, 13 | Chapter = 2, 14 | Series = 3, 15 | Manga = 4, 16 | } 17 | 18 | public enum LibraryType 19 | { 20 | None, 21 | Local, 22 | Ftp 23 | } 24 | 25 | [MessagePackObject] 26 | public partial class FileTreeNode 27 | { 28 | [Key(0)] public long ObjectId { get; set; } 29 | [Key(1)] public long ParentId { get; set; } 30 | [Key(2)] public string FullName { get; set; } 31 | [Key(3)] public bool IsDirectory { get; set; } 32 | [Key(4)] public FileStructType StructType { get; set; } 33 | [Key(5)] public string Authentication { get; set; } 34 | 35 | [IgnoreMember] 36 | public string Name => IsDirectory 37 | ? FullName.Split(Path.DirectorySeparatorChar).Last() 38 | : Path.GetFileNameWithoutExtension(FullName); 39 | } 40 | 41 | [MessagePackObject] 42 | public partial class FileTreeRoot 43 | { 44 | [Key(0)] public long ObjectId { get; set; } 45 | [Key(1)] public string Name { get; set; } 46 | [Key(2)] public string Host { get; set; } 47 | [Key(3)] public ushort Port { get; set; } 48 | [Key(4)] public string Path { get; set; } 49 | [Key(5)] public string Scheme { get; set; } 50 | [Key(6)] public string Username { get; set; } 51 | [Key(7)] public string Password { get; set; } 52 | [Key(8)] public string ScraperName { get; set; } 53 | } 54 | } -------------------------------------------------------------------------------- /Otokoneko.Base/DataType/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MessagePack; 4 | #if SERVER 5 | using SqlSugar; 6 | #endif 7 | 8 | namespace Otokoneko.DataType 9 | { 10 | public enum ServerNotify 11 | { 12 | Unknown, 13 | NewMessageSent 14 | } 15 | 16 | [MessagePackObject] 17 | public class Message 18 | { 19 | #if SERVER 20 | [SugarColumn(IsPrimaryKey = true)] 21 | #endif 22 | [Key(0)] 23 | public long ObjectId { get; set; } 24 | #if SERVER 25 | [SugarColumn(IsOnlyIgnoreUpdate = true)] 26 | #endif 27 | [Key(1)] 28 | public DateTime CreateUtcTime { get; set; } 29 | [Key(2)] 30 | public long SenderId { get; set; } 31 | #if SERVER 32 | [SugarColumn(IsNullable = true)] 33 | #endif 34 | [Key(3)] 35 | public string Data { get; set; } 36 | #if SERVER 37 | [SugarColumn(IsIgnore = true)] 38 | #endif 39 | [IgnoreMember] 40 | public List Receivers { get; set; } 41 | [Key(4)] 42 | public bool Checked { get; set; } 43 | } 44 | 45 | [MessagePackObject] 46 | public class MessageUserMapping 47 | { 48 | #if SERVER 49 | [SugarColumn(UniqueGroupNameList = new[] { nameof(MessageId), nameof(ReceiverId) })] 50 | #endif 51 | [Key(0)] 52 | public long MessageId { get; set; } 53 | #if SERVER 54 | [SugarColumn(UniqueGroupNameList = new[] { nameof(MessageId), nameof(ReceiverId) })] 55 | #endif 56 | [Key(1)] 57 | public long ReceiverId { get; set; } 58 | [Key(2)] 59 | public bool Checked { get; set; } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Otokoneko.Base/DataType/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MessagePack; 4 | 5 | namespace Otokoneko.DataType 6 | { 7 | [MessagePackObject] 8 | public class PluginParameter 9 | { 10 | [Key(0)] 11 | public Type Type { get; set; } 12 | [Key(1)] 13 | public string Name { get; set; } 14 | [Key(2)] 15 | public string Alias { get; set; } 16 | [Key(3)] 17 | public object Value { get; set; } 18 | [Key(4)] 19 | public bool IsReadOnly { get; set; } 20 | [Key(5)] 21 | public string Comment { get; set; } 22 | } 23 | 24 | [MessagePackObject] 25 | public class PluginDetail 26 | { 27 | [Key(0)] 28 | public string Name { get; set; } 29 | [Key(1)] 30 | public Version Version { get; set; } 31 | [Key(2)] 32 | public string Author { get; set; } 33 | [Key(3)] 34 | public string Type { get; set; } 35 | [Key(4)] 36 | public List SupportInterface { get; set; } 37 | [Key(5)] 38 | public List RequiredParameters { get; set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Otokoneko.Base/DataType/ScheduleTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MessagePack; 4 | 5 | namespace Otokoneko.DataType 6 | { 7 | public enum TaskStatus 8 | { 9 | None, 10 | Waiting, 11 | Executing, //任何任务在执行Execute()函数时的状态 12 | Running, //拥有子任务的任务在执行Execute()函数完成后,任务完成前的状态 13 | Fail, 14 | Success, 15 | Canceled 16 | } 17 | 18 | [Union(0, typeof(CronTrigger))] 19 | [Union(1, typeof(DisposableTrigger))] 20 | [Union(2, typeof(TaskCompletedTrigger))] 21 | [MessagePackObject] 22 | public abstract partial class ScheduleTaskTrigger 23 | { 24 | [IgnoreMember] 25 | public static HashSet SupportTriggerTypes { get; } = new HashSet() 26 | { 27 | typeof(DisposableTrigger), 28 | typeof(CronTrigger), 29 | typeof(TaskCompletedTrigger), 30 | }; 31 | } 32 | 33 | [MessagePackObject] 34 | public partial class CronTrigger : ScheduleTaskTrigger 35 | { 36 | [Key(0)] 37 | public DateTime StartDateTime { get; set; } 38 | [Key(1)] 39 | public int Interval { get; set; } 40 | } 41 | 42 | [MessagePackObject] 43 | public partial class DisposableTrigger : ScheduleTaskTrigger 44 | { 45 | [Key(0)] 46 | public bool Triggered { get; set; } 47 | } 48 | 49 | [MessagePackObject] 50 | public partial class TaskCompletedTrigger: ScheduleTaskTrigger 51 | { 52 | [Key(0)] 53 | public long PlanId { get; set; } 54 | } 55 | 56 | [MessagePackObject] 57 | public partial class DisplayTask 58 | { 59 | [Key(0)] 60 | public long ObjectId { get; set; } 61 | [Key(1)] 62 | public string Name { get; set; } 63 | [Key(2)] 64 | public TaskStatus Status { get; set; } 65 | [Key(3)] 66 | public double Progress { get; set; } 67 | [Key(4)] 68 | public DateTime CreateTime { get; set; } 69 | [Key(5)] 70 | public bool HasChild { get; set; } 71 | } 72 | 73 | [Union(0, typeof(DownloadPlan))] 74 | [Union(1, typeof(ScanPlan))] 75 | [MessagePackObject] 76 | public abstract partial class Plan 77 | { 78 | [Key(0)] 79 | public long ObjectId { get; set; } 80 | [Key(1)] 81 | public string Name { get; set; } 82 | [Key(2)] 83 | public bool Enable { get; set; } 84 | [Key(3)] 85 | public ScheduleTaskTrigger Trigger { get; set; } 86 | [Key(4)] 87 | public DateTime? LastTriggeredTime { get; set; } 88 | 89 | [IgnoreMember] 90 | public static HashSet SupportPlanTypes { get; } = new HashSet() 91 | { 92 | typeof(DownloadPlan), 93 | typeof(ScanPlan), 94 | }; 95 | } 96 | 97 | [MessagePackObject] 98 | public sealed partial class DownloadPlan: Plan 99 | { 100 | [Key(5)] 101 | public string LibraryPath { get; set; } 102 | [Key(6)] 103 | public string Urls { get; set; } 104 | } 105 | 106 | [MessagePackObject] 107 | public sealed partial class ScanPlan : Plan 108 | { 109 | [Key(5)] 110 | public long LibraryId { get; set; } 111 | } 112 | } -------------------------------------------------------------------------------- /Otokoneko.Base/DataType/Search.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | 3 | namespace Otokoneko.DataType 4 | { 5 | public enum OrderType 6 | { 7 | Default, 8 | CreateTime, 9 | UpdateTime 10 | } 11 | 12 | public enum QueryType 13 | { 14 | Keyword, 15 | Favorite, 16 | History 17 | } 18 | 19 | 20 | [MessagePackObject] 21 | public class MangaQueryHelper 22 | { 23 | [Key(0)] 24 | public string QueryString { get; set; } 25 | [Key(1)] 26 | public int Offset { get; set; } 27 | [Key(2)] 28 | public int Limit { get; set; } 29 | [Key(3)] 30 | public QueryType QueryType { get; set; } 31 | [Key(4)] 32 | public OrderType OrderType { get; set; } 33 | [Key(5)] 34 | public bool Asc { get; set; } 35 | } 36 | 37 | [MessagePackObject] 38 | public class TagQueryHelper 39 | { 40 | [Key(0)] 41 | public string QueryString { get; set; } 42 | [Key(1)] 43 | public int Offset { get; set; } 44 | [Key(2)] 45 | public int Limit { get; set; } 46 | [Key(3)] 47 | public long TypeId { get; set; } 48 | } 49 | } -------------------------------------------------------------------------------- /Otokoneko.Base/DataType/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MessagePack; 3 | #if SERVER 4 | using SqlSugar; 5 | #endif 6 | 7 | namespace Otokoneko.DataType 8 | { 9 | public enum SessionKeepTime 10 | { 11 | OneDay, 12 | OneWeek, 13 | OneMonth, 14 | OneYear 15 | }; 16 | 17 | public enum RegisterResult 18 | { 19 | Unknown, 20 | Success, 21 | InvitationCodeNotFound, 22 | InvitationCodeHasBeenUsed, 23 | UsernameRepeated 24 | } 25 | 26 | public enum UserAuthority 27 | { 28 | Root = 0, 29 | Admin = 1, 30 | User = 100, 31 | Banned = int.MaxValue - 1, 32 | Visitor = int.MaxValue 33 | } 34 | 35 | [MessagePackObject] 36 | public class User 37 | { 38 | #if SERVER 39 | [SugarColumn(IsPrimaryKey = true)] 40 | #endif 41 | [Key(0)] 42 | public long Id { get; set; } 43 | #if SERVER 44 | [SugarColumn(UniqueGroupNameList = new[] { nameof(Name) })] 45 | #endif 46 | [Key(1)] 47 | public string Name { get; set; } 48 | [IgnoreMember] 49 | public byte[] Salt { get; set; } 50 | [IgnoreMember] 51 | public string Password { get; set; } 52 | [Key(2)] 53 | public UserAuthority Authority { get; set; } 54 | } 55 | 56 | [MessagePackObject] 57 | public class Invitation 58 | { 59 | #if SERVER 60 | [SugarColumn(IsPrimaryKey = true)] 61 | #endif 62 | [Key(0)] 63 | public long ObjectId { get; set; } 64 | [IgnoreMember] 65 | public long SenderId { get; set; } 66 | #if SERVER 67 | [SugarColumn(IsIgnore = true)] 68 | #endif 69 | [Key(1)] 70 | public User Sender { get; set; } 71 | [Key(2)] 72 | public UserAuthority Authority { get; set; } 73 | [Key(3)] 74 | public string InvitationCode { get; set; } 75 | [IgnoreMember] 76 | public long ReceiverId { get; set; } 77 | #if SERVER 78 | [SugarColumn(IsIgnore = true)] 79 | #endif 80 | [Key(4)] 81 | public User Receiver { get; set; } 82 | [Key(5)] 83 | public DateTime CreateTime { get; set; } 84 | [Key(6)] 85 | public DateTime UsedTime { get; set; } 86 | } 87 | 88 | [MessagePackObject] 89 | public class UserHelper 90 | { 91 | [Key(0)] 92 | public string Name { get; set; } 93 | [Key(1)] 94 | public string Password { get; set; } 95 | [Key(2)] 96 | public SessionKeepTime SessionKeepTime { get; set; } 97 | [Key(3)] 98 | public string InvitationCode { get; set; } 99 | } 100 | } -------------------------------------------------------------------------------- /Otokoneko.Base/Otokoneko.Base.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 82b38b9c-f381-4335-8f3e-f8eb6f3f10ee 7 | 8 | 9 | Otokoneko 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Otokoneko.Base/Otokoneko.Base.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 82b38b9c-f381-4335-8f3e-f8eb6f3f10ee 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Otokoneko.Base/Server.cs: -------------------------------------------------------------------------------- 1 | #if SERVER 2 | using Microsoft.Extensions.Hosting; 3 | using Otokoneko.Base.Network; 4 | using SuperSocket; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Security.Authentication; 8 | using System.Security.Cryptography.X509Certificates; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Logging; 11 | using System.Net.NetworkInformation; 12 | using System.Linq; 13 | using System.Threading; 14 | 15 | namespace Otokoneko.Server 16 | { 17 | public partial class Server 18 | { 19 | private IHost _host; 20 | private readonly MessageEncoder _responseEncoder = new MessageEncoder(); 21 | 22 | private void CreateHost(ushort port, X509Certificate certificate) 23 | { 24 | _host = 25 | SuperSocketHostBuilder.Create() 26 | .UsePackageHandler(async (session, request) => 27 | { 28 | try 29 | { 30 | var result = await ProcessRequest(request, session); 31 | switch (result) 32 | { 33 | case Response response: 34 | await session.SendAsync(_responseEncoder, response); 35 | break; 36 | case Responses responses: 37 | { 38 | await foreach (var response in responses) 39 | { 40 | await session.SendAsync(_responseEncoder, response); 41 | } 42 | 43 | break; 44 | } 45 | } 46 | } 47 | catch (Exception e) 48 | { 49 | Logger.Warn(e); 50 | } 51 | }) 52 | .ConfigureSuperSocket((option) => 53 | { 54 | option.Name = "Otokoneko Manga Server"; 55 | option.Listeners = new List() 56 | { 57 | new ListenOptions() 58 | { 59 | Security = SslProtocols.None, 60 | Ip = "Any", 61 | Port = port, 62 | BackLog = 100, 63 | CertificateOptions = new CertificateOptions() 64 | { 65 | Certificate = certificate 66 | } 67 | } 68 | }; 69 | option.MaxPackageLength = 64 * 1024 * 1024; 70 | }) 71 | .ConfigureLogging((hostCtx, loggingBuilder) => 72 | { 73 | loggingBuilder.ClearProviders(); 74 | }) 75 | .Build(); 76 | } 77 | 78 | public void Run() 79 | { 80 | Logger.Info($"开始监听端口: {Port}"); 81 | _host.Start(); 82 | } 83 | 84 | public async Task Close() 85 | { 86 | await _host.StopAsync(); 87 | _host.Dispose(); 88 | } 89 | 90 | public bool PortInUse() 91 | { 92 | return IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners().Any(it => it.Port == Port); 93 | } 94 | } 95 | } 96 | #endif -------------------------------------------------------------------------------- /Otokoneko.Base/Utils/AsyncQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Priority_Queue; 5 | 6 | namespace Otokoneko.Utils 7 | { 8 | public class AsyncQueue where TPriority : IComparable 9 | { 10 | private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1); 11 | private readonly SemaphoreSlim _full = new SemaphoreSlim(0); 12 | private readonly SimplePriorityQueue _priorityQueue = new SimplePriorityQueue(); 13 | 14 | public async ValueTask Enqueue(T item, TPriority priority) 15 | { 16 | await _mutex.WaitAsync(); 17 | try 18 | { 19 | _priorityQueue.Enqueue(item, priority); 20 | _full.Release(); 21 | } 22 | finally 23 | { 24 | _mutex.Release(); 25 | } 26 | } 27 | 28 | public async ValueTask Dequeue() 29 | { 30 | await _full.WaitAsync(); 31 | await _mutex.WaitAsync(); 32 | try 33 | { 34 | var result = _priorityQueue.Dequeue(); 35 | return result; 36 | } 37 | finally 38 | { 39 | _mutex.Release(); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Otokoneko.Base/Utils/PasswordHashProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace Otokoneko.Utils 5 | { 6 | public static class PasswordHashProvider 7 | { 8 | private const int HashByteSize = 64; 9 | 10 | public static string CreateHash(string username, string password, int iterations = 1 << 15) 11 | { 12 | using var sha512 = SHA512.Create(); 13 | var salt = sha512.ComputeHash(Encoding.UTF8.GetBytes(username)); 14 | var hash = Pbkdf2(password, salt, iterations, HashByteSize); 15 | var sb = new StringBuilder(); 16 | foreach (var _ in hash) 17 | { 18 | sb.Append(_.ToString("X2")); 19 | } 20 | return sb.ToString(); 21 | } 22 | 23 | public static string CreateHash(byte[] salt, string password, int iterations = 1 << 15) 24 | { 25 | using var sha512 = SHA512.Create(); 26 | var hash = Pbkdf2(password, salt, iterations, HashByteSize); 27 | var sb = new StringBuilder(); 28 | foreach (var _ in hash) 29 | { 30 | sb.Append(_.ToString("X2")); 31 | } 32 | return sb.ToString(); 33 | } 34 | 35 | private static byte[] Pbkdf2(string password, byte[] salt, int iterations, int outputBytes) 36 | { 37 | using var pbkdf2 = new Rfc2898DeriveBytes(password, salt) { IterationCount = iterations }; 38 | return pbkdf2.GetBytes(outputBytes); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Otokoneko.Base/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Otokoneko.Base.Network; 4 | 5 | message Request { 6 | int32 Id = 1; 7 | string Method = 2; 8 | string Token = 3; 9 | Message Data = 4; 10 | } 11 | 12 | message Response { 13 | int32 Id = 1; 14 | bool Completed = 2; 15 | ResponseStatus Status = 3; 16 | Message Data = 4; 17 | Message ServerMessage = 5; 18 | } 19 | 20 | message Message { 21 | int64 ObjectId = 2; 22 | int32 Offset = 3; 23 | bytes Data = 1; 24 | } 25 | 26 | enum ResponseStatus { 27 | Success = 0; 28 | NotFound = 1; 29 | Forbidden = 2; 30 | BadRequest = 3; 31 | }; 32 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | 4 | namespace Otokoneko.Client.WPFClient 5 | { 6 | /// 7 | /// Interaction logic for App.xaml 8 | /// 9 | public partial class App : Application 10 | { 11 | protected override void OnStartup(StartupEventArgs e) 12 | { 13 | DispatcherUnhandledException += Application_DispatcherUnhandledException; 14 | base.OnStartup(e); 15 | } 16 | 17 | private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) 18 | { 19 | var crashReporter = new View.CrashReporter("出现未捕获的异常", e.Exception); 20 | crashReporter.ShowDialog(); 21 | Environment.Exit(-1); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Model/DataType.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Media; 2 | using MessagePack; 3 | 4 | namespace Otokoneko.DataType 5 | { 6 | public partial class TagType 7 | { 8 | private static Client.Model Model { get; set; } = Client.Model.Instance; 9 | 10 | private Color? _color; 11 | 12 | [IgnoreMember] 13 | public Color Color 14 | { 15 | get => _color ??= Model.GetColor(ObjectId); 16 | set 17 | { 18 | _color = value; 19 | Model.SetColor(ObjectId, (Color)_color); 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Model/Setting.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MessagePack; 3 | using Otokoneko.DataType; 4 | 5 | namespace Otokoneko.Client 6 | { 7 | [MessagePackObject(true)] 8 | public class SearchOption 9 | { 10 | public bool Pageable { get; set; } = true; 11 | public int PageSize { get; set; } = 35; 12 | public OrderType OrderType { get; set; } = OrderType.UpdateTime; 13 | public bool Asc { get; set; } = false; 14 | } 15 | 16 | public enum WindowMode 17 | { 18 | NormalWindow, 19 | FullScreen, 20 | BorderlessWindow, 21 | BorderlessWindowWithControlButton, 22 | } 23 | 24 | public enum AutoCropMode 25 | { 26 | None, 27 | RightToLeft, 28 | LeftToRight, 29 | } 30 | 31 | [MessagePackObject(true)] 32 | public class WindowOption 33 | { 34 | public WindowMode WindowMode { get; set; } = WindowMode.BorderlessWindowWithControlButton; 35 | public bool ShowWindowControlButton { get; set; } = false; 36 | } 37 | 38 | public enum ImageDisplayMode 39 | { 40 | ImageListMode, 41 | SinglePageMode, 42 | DoublePageMode 43 | } 44 | 45 | public enum ScaleMode 46 | { 47 | Auto, 48 | FitWidth, 49 | FitHeight, 50 | Locked 51 | } 52 | 53 | [MessagePackObject(true)] 54 | public class ThemeOption 55 | { 56 | public bool DarkMode { get; set; } = true; 57 | public string Color { get; set; } = "Purple"; 58 | } 59 | 60 | [MessagePackObject(true)] 61 | public class MangaReadOption 62 | { 63 | public WindowOption WindowOption { get; set; } = new WindowOption(); 64 | public ImageDisplayMode ImageDisplayMode { get; set; } = ImageDisplayMode.ImageListMode; 65 | public AutoCropMode AutoCropMode { get; set; } = AutoCropMode.RightToLeft; 66 | public ScaleMode ScaleMode { get; set; } = ScaleMode.Locked; 67 | public double ScaleValue { get; set; } = 1; 68 | } 69 | 70 | [MessagePackObject(true)] 71 | public class TagDisplayOption 72 | { 73 | public Dictionary TagTypeColorDictionary { get; set; } = new Dictionary(); 74 | public double FontSize { get; set; } = 14; 75 | } 76 | 77 | [MessagePackObject(true)] 78 | public class CacheOption 79 | { 80 | public long MaxFileCacheSize { get; set; } = 50 * 1024 * 1024; 81 | } 82 | 83 | [MessagePackObject(true)] 84 | public class Setting 85 | { 86 | public SearchOption SearchOption { get; set; } = new SearchOption(); 87 | public MangaReadOption MangaReadOption { get; set; } = new MangaReadOption(); 88 | public TagDisplayOption TagDisplayOption { get; set; } = new TagDisplayOption(); 89 | public ThemeOption ThemeOption { get; set; } = new ThemeOption(); 90 | public CacheOption CacheOption { get; set; } = new CacheOption(); 91 | 92 | public Setting Copy() 93 | { 94 | var bytes = MessagePackSerializer.Serialize(this); 95 | return MessagePackSerializer.Deserialize(bytes); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Otokoneko.Client.WPFClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30717.126 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Otokoneko.Client.WPFClient", "Otokoneko.Client.WPFClient.csproj", "{ACC5C15D-7EC0-464E-9683-013A741759AF}" 7 | EndProject 8 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Otokoneko.Base", "..\Otokoneko.Base\Otokoneko.Base.shproj", "{82B38B9C-F381-4335-8F3E-F8EB6F3F10EE}" 9 | EndProject 10 | Global 11 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 12 | ..\Otokoneko.Base\Otokoneko.Base.projitems*{82b38b9c-f381-4335-8f3e-f8eb6f3f10ee}*SharedItemsImports = 13 13 | ..\Otokoneko.Base\Otokoneko.Base.projitems*{acc5c15d-7ec0-464e-9683-013a741759af}*SharedItemsImports = 5 14 | EndGlobalSection 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {ACC5C15D-7EC0-464E-9683-013A741759AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {ACC5C15D-7EC0-464E-9683-013A741759AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {ACC5C15D-7EC0-464E-9683-013A741759AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {ACC5C15D-7EC0-464E-9683-013A741759AF}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {503EE739-7571-4242-A547-D759BED75C6C} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\win-x86 10 | FileSystem 11 | net5.0-windows 12 | win-x86 13 | true 14 | True 15 | False 16 | 17 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Utils/DependencyObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | namespace Otokoneko.Client.WPFClient.Utils 5 | { 6 | public static class DependencyObjectExtensions 7 | { 8 | public static T GetChild(this DependencyObject o) 9 | { 10 | if (o is T s) return s; 11 | for (var i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++) 12 | { 13 | var child = VisualTreeHelper.GetChild(o, i); 14 | var result = GetChild(child); 15 | if (result == null) continue; 16 | return result; 17 | } 18 | return default; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Utils/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | 4 | namespace Otokoneko.Client.WPFClient.Utils 5 | { 6 | public static class FileUtils 7 | { 8 | public static void OpenFolder(string folderPath) 9 | { 10 | folderPath = Path.GetFullPath(folderPath); 11 | if (Directory.Exists(folderPath)) 12 | { 13 | ProcessStartInfo startInfo = new() 14 | { 15 | Arguments = folderPath, 16 | FileName = "explorer.exe" 17 | }; 18 | 19 | Process.Start(startInfo); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Utils/FormatUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Otokoneko.Client.WPFClient.Utils 5 | { 6 | public static class FormatUtils 7 | { 8 | private static readonly string[] SizeSuffixes = { "KB", "MB", "GB" }; 9 | 10 | public static string FormatLocalDateTime(DateTime? localDateTime) 11 | { 12 | if (localDateTime == null) return string.Empty; 13 | var time = (DateTime)localDateTime; 14 | return 15 | $"{time.Year:D4}.{time.Month:D2}.{time.Day:D2} {time.Hour:D2}:{time.Minute:D2}"; 16 | } 17 | 18 | public static string FormatSizeOfBytes(double size) 19 | { 20 | foreach (var suffix in SizeSuffixes) 21 | { 22 | size /= 1024; 23 | if (size < 1024) 24 | { 25 | return $"{size:F} {suffix}"; 26 | } 27 | } 28 | return $"{size:F} {SizeSuffixes.Last()}"; 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Utils/ImageUtils.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Windows.Media.Imaging; 3 | 4 | namespace Otokoneko.Client.WPFClient.Utils 5 | { 6 | public class ImageUtils 7 | { 8 | public static BitmapImage Convert(byte[] imageContent) 9 | { 10 | if (imageContent == null) return null; 11 | var memoryStream = new MemoryStream(imageContent); 12 | var image = new BitmapImage(); 13 | image.BeginInit(); 14 | image.CacheOption = BitmapCacheOption.OnLoad; 15 | image.StreamSource = memoryStream; 16 | image.EndInit(); 17 | image.Freeze(); 18 | memoryStream.Close(); 19 | memoryStream.Dispose(); 20 | return image; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/Utils/MathUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Otokoneko.Client.WPFClient.Utils 4 | { 5 | public static class MathUtils 6 | { 7 | public static T Min(T v1, T v2) where T : IComparable 8 | { 9 | return v1.CompareTo(v2) < 0 ? v1 : v2; 10 | } 11 | 12 | public static T Max(T v1, T v2) where T : IComparable 13 | { 14 | return v1.CompareTo(v2) > 0 ? v1 : v2; 15 | } 16 | 17 | public static T LimitValue(T origin, T min, T max) where T : IComparable 18 | { 19 | return Max(Min(origin, max), min); 20 | } 21 | 22 | public static bool AlmostEqual(double x, double y, double EPS) 23 | { 24 | return Math.Abs(x - y) <= EPS; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/View/AlwaysRefreshDataTemplateSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace Otokoneko.Client.WPFClient.View 5 | { 6 | class AlwaysRefreshDataTemplateSelector : DataTemplateSelector 7 | { 8 | public override DataTemplate SelectTemplate(object item, DependencyObject container) 9 | { 10 | var declaredDataTemplate = FindDeclaredDataTemplate(item, container); 11 | if (declaredDataTemplate == null) return null; 12 | var wrappedDataTemplate = WrapDataTemplate(declaredDataTemplate); 13 | return wrappedDataTemplate; 14 | } 15 | 16 | private static DataTemplate WrapDataTemplate(DataTemplate declaredDataTemplate) 17 | { 18 | var frameworkElementFactory = new FrameworkElementFactory(typeof(ContentPresenter)); 19 | frameworkElementFactory.SetValue(ContentPresenter.ContentTemplateProperty, declaredDataTemplate); 20 | var dataTemplate = new DataTemplate { VisualTree = frameworkElementFactory }; 21 | return dataTemplate; 22 | } 23 | 24 | private static DataTemplate FindDeclaredDataTemplate(object item, DependencyObject container) 25 | { 26 | if (item == null) return null; 27 | var dataTemplateKey = new DataTemplateKey(item.GetType()); 28 | var dataTemplate = ((FrameworkElement)container).FindResource(dataTemplateKey) as DataTemplate; 29 | return dataTemplate; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/View/Behavior.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Input; 4 | using Microsoft.Xaml.Behaviors; 5 | 6 | namespace Otokoneko.Client.WPFClient.View 7 | { 8 | public class OnlyReceiveIntBehavior : Behavior 9 | { 10 | protected override void OnAttached() 11 | { 12 | base.OnAttached(); 13 | AssociatedObject.PreviewTextInput += AssociatedObjectOnPreviewTextInput; 14 | DataObject.AddPastingHandler(AssociatedObject, PastingHandler); 15 | } 16 | 17 | private void PastingHandler(object sender, DataObjectPastingEventArgs e) 18 | { 19 | if (e.DataObject.GetDataPresent(typeof(string))) 20 | { 21 | var text = e.DataObject.GetData(typeof(string)) as string; 22 | if (!int.TryParse(text, out _)) 23 | { 24 | e.CancelCommand(); 25 | } 26 | } 27 | else 28 | { 29 | e.CancelCommand(); 30 | } 31 | } 32 | 33 | private void AssociatedObjectOnPreviewTextInput(object sender, TextCompositionEventArgs e) 34 | { 35 | if (!int.TryParse(e.Text, out _)) 36 | { 37 | AssociatedObject.Text = AssociatedObject.Text.Replace(e.Text, ""); 38 | AssociatedObject.CaretIndex = AssociatedObject.Text.Length; 39 | e.Handled = true; 40 | } 41 | } 42 | } 43 | 44 | public class BubbleScrollEventToParent : Behavior 45 | { 46 | protected override void OnAttached() 47 | { 48 | base.OnAttached(); 49 | AssociatedObject.PreviewMouseWheel += AssociatedObjectOnPreviewMouseWheel; 50 | } 51 | 52 | private void AssociatedObjectOnPreviewMouseWheel(object sender, MouseWheelEventArgs e) 53 | { 54 | if (e.Handled) return; 55 | e.Handled = true; 56 | var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) 57 | { 58 | RoutedEvent = UIElement.MouseWheelEvent, Source = sender 59 | }; 60 | var parent = ((Control)sender).Parent as UIElement; 61 | parent?.RaiseEvent(eventArg); 62 | } 63 | } 64 | 65 | public static class ScrollViewerBehavior 66 | { 67 | public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), typeof(ScrollViewerBehavior), new UIPropertyMetadata(0.0, OnVerticalOffsetChanged)); 68 | public static void SetVerticalOffset(FrameworkElement target, double value) => target.SetValue(VerticalOffsetProperty, value); 69 | public static double GetVerticalOffset(FrameworkElement target) => (double)target.GetValue(VerticalOffsetProperty); 70 | private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) => (target as ScrollViewer)?.ScrollToVerticalOffset((double)e.NewValue); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Otokoneko.Client.WPFClient/View/CommentWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 22 | 27 | 32 |