Compare commits

..
19 Commits
Author SHA1 Message Date
rmbgame d382d70487 修改部分提示文本,修复证书生成安装失败 2021-02-21 21:37:47 +08:00
rmbgame c438656378 修改证书生成方式避免324错误和100错误 2021-02-21 16:50:44 +08:00
Aigio Liu 035b1a6421 更新 Readme 2021-02-20 23:10:54 +08:00
Aigio Liu f3db1a23ed 优化MSBuild配置 2021-02-20 20:27:32 +08:00
Aigio Liu 59096c898c Merge branch 'develop' of https://github.com/rmbadmin/SteamTools into develop 2021-02-20 17:02:25 +08:00
Aigio Liu 4d76decf73 V1.2 新构架 2021-02-20 16:45:44 +08:00
Aigio Liu c2d95a1833 V1.2 新构架 2021-02-20 16:45:05 +08:00
rmbgame fd6f5ff3ea 更新nuge包 2021-02-20 10:51:16 +08:00
rmbgame 5bd381f553 修复faq文本错误 2021-02-20 10:39:25 +08:00
rmbgame 55bad031df 修复在某些情况下出现算法不同证书验证错误问题 2021-02-20 00:47:40 +08:00
rmbgame 866ad1a821 库存游戏列表添加clienticon图标显示
无边框窗口化功能添加操作窗口历史记录
2021-01-30 20:34:58 +08:00
rmbgame 9fbe6408ab 修复某些情况下开机自启失效问题
修复配置文件加载时提示根元素错误
配置文件备份机制完善
2021-01-29 19:08:11 +08:00
rmbgame c8ea806360 关于里加入FAQ 2021-01-28 23:11:18 +08:00
rmbgame a8b0f9877c 新增账号切换登录新账号功能
账号切换可以实时刷新新登录的账号了
2021-01-28 20:08:43 +08:00
rmbgame d82f94f95d 修改谷歌验证码代理方式为全局跳转www.recaptcha.net 2021-01-27 19:06:00 +08:00
rmbgame a40fb84783 记录更详细错误日志 2021-01-27 14:34:34 +08:00
rmbgame c9cbf696e4 新增Steam图片上传的代理选项 2021-01-25 17:22:43 +08:00
rmbgame cc86913b72 修复某些情况下配置读取错误 2021-01-23 00:02:17 +08:00
rmbgame ba02250f61 update readme 2021-01-22 23:32:03 +08:00
407 changed files with 43382 additions and 1314 deletions
+12
View File
@@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition="Exists('..\Unicorn.pfx')">
<SignAssembly>true</SignAssembly>
<DelaySign>false</DelaySign>
<AssemblyOriginatorKeyFile>..\Unicorn.pfx</AssemblyOriginatorKeyFile>
</PropertyGroup>
</Project>
+73 -69
View File
@@ -1,9 +1,8 @@
<img src="https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/icon/LOGO1.png" alt="logo" width="130" height="130" align="left" />
<h1 align="center">Steam++ Tools 🧰</h1>
<h1>Steam++ Tools</h1>
<div align="center">
> 「Steam++」is a toolkit that contains a variety of Steam tools, most of which require you to download and install Steam in order to use them.
<br/>
「Steam++」is a toolkit that contains a variety of Steam tools, most of which require you to download and install Steam in order to use them.
![Release Download](https://img.shields.io/github/downloads/rmbadmin/SteamTools/latest/total?style=flat-square)
[![Release Version](https://img.shields.io/github/v/release/rmbadmin/SteamTools?style=flat-square)](https://github.com/rmbadmin/SteamTools/releases/latest)
@@ -12,76 +11,65 @@
[![GitHub Fork](https://img.shields.io/github/forks/rmbadmin/SteamTools?style=flat-square)](https://github.com/rmbadmin/SteamTools/network/members)
![GitHub repo size](https://img.shields.io/github/repo-size/rmbadmin/SteamTools?style=flat-square&color=3cb371)
## Samples
-------
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/s.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/e.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/e2.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/t.png)
------
</div>
## Core Functions
English | [简体中文](./README.md)
### 1. Local reverse proxy for Steam's community webpage to enable normal access in mainland China
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/1.png)
---
### 2. Quickly switch the Steam account that the current PC has remembered to log into
## ✨ Functions
1. Local reverse proxy for Steam's community webpage to enable normal access in **the Chinese Mainland, Mainland of China**
2. Quickly switch the Steam account that the current PC has remembered to log into
- This feature is to read the local user login records stored under the Steam path to directly display the operation, you can switch between multiple accounts without re-entering passwords and tokens.
3. Achievement stat modifications for Steam games
- Function reference SteamAchievementManager for secondary development, modified the game list loading and operation ease of use.
4. Steam Local Two-Step Authenticator
- The function reference [WinAuth](https://github.com/winauth/winauth) is developed to enable you to view your token without launching the mobile version of Steam App, similar function software are [WinAuth](https://github.com/winauth/winauth), [SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator).
5. Some game tools
- Currently there is a forced game borderless windowing, CSGO fix VAC shield.
This feature is to read the local user login records stored under the Steam path to directly display the operation, you can switch between multiple accounts without re-entering passwords and tokens.
## 🖥 Operating Environment
- Windows 10 1607+
- Windows 8.1 / Windows Server 2012 R2
- Prerequisites
- Microsoft Visual C++ 2015-2019 Redistributable [64 bit](https://aka.ms/vs/16/release/vc_redist.x64.exe) / [32 bit](https://aka.ms/vs/16/release/vc_redist.x86.exe)
- Windows 7 SP1 [ESU](https://docs.microsoft.com/troubleshoot/windows-client/windows-7-eos-faq/windows-7-extended-security-updates-faq)
- Prerequisites
- Microsoft Visual C++ 2015-2019 Redistributable [64 bit](https://aka.ms/vs/16/release/vc_redist.x64.exe) / [32 bit](https://aka.ms/vs/16/release/vc_redist.x86.exe)
- KB3063858 [64 bit](https://www.microsoft.com/download/details.aspx?id=47442) / [32 bit](https://www.microsoft.com/download/details.aspx?id=47409)
- ~~macOS 10.13 High Sierra Or Higher~~
<!--
- ~~Alpine Linux 3.11+~~
- ~~CentOS 7+~~
- ~~Debian 9+~~
- ~~Fedora 32+~~
- ~~Linux Mint 18+~~
- ~~openSUSE 15+~~
- ~~Red Hat Enterprise Linux 7+~~
- ~~SUSE Enterprise Linux (SLES) 12 SP2+~~
- ~~Ubuntu 20.10, 20.04, 18.04, 16.04~~
- ~~Android 5.0+~~
- ~~iOS 8.0+~~
-->
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/2.png)
---
### 3. Achievement stat modifications for Steam games
Function reference [SteamAchievementManager](https://github.com/gibbed/SteamAchievementManager) for secondary development, modified the game list loading and operation ease of use.
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/3.png)
---
## 🌎 Roadmap
Read what we [milestones](https://github.com/SteamTools-Team/SteamTools/milestones), and feel free to ask questions.
### 4. Steam Local Two-Step Authenticator
The function reference [WinAuth](https://github.com/winauth/winauth) is developed to enable you to view your token without launching the mobile version of Steam App, similar function software are [WinAuth](https://github.com/winauth/winauth), [SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator).
! [](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/4.png)
## ⌨️ Development Environment
[Visual Studio 2019 Version 16.8 Or Higher](https://visualstudio.microsoft.com/vs/) Or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/) Or [JetBrains Rider](https://www.jetbrains.com/rider/) Or [Visual Studio Code](https://code.visualstudio.com/)
- Workload
- Web and Cloud
- ASP.NET and Web Development
- Desktop and Mobile Applications
- .NET Desktop Development
- Mobile Development using .Net
- Other Toolsets
- .NET Core Cross Platform Development
- Single Component
- GitHub Extension for Visual Studio
- [Visual Studio Marketplace](https://marketplace.visualstudio.com/)
- [Avalonia for Visual Studio](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio)
- [NUnit VS Templates](https://marketplace.visualstudio.com/items?itemName=NUnitDevelopers.NUnitTemplatesforVisualStudio)
---
### 5. Some game tools
Currently there is a forced game borderless windowing, CSGO fix VAC shield.
This piece is randomly do some of my often used or idle tinkering with the function.
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/5.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/6.png)
---
## Features expected to be added subsequently
### Steam Idle Card
The goal is to enable direct access to badge card information and start hanging cards without logging into your Steam account within the software.
### Plug-in form to load and run ASF
Support ASF as a plug-in to run within the tool and enhance the use of ASF in Windows Desktop environment.
### Steam Custom Cover Management
Enhanced management of Steam custom covers and quick matching of downloaded app covers from [SteamGridDB](https://www.steamgriddb.com/).
### More other game platform features
### More non-cheat game features
---
## Operating Environment
> The program was developed using C# WPF in .,If it does not work please download and install[.NET Framework 4.7.2](https://dotnet.microsoft.com/download/dotnet-framework/net472)。
> The missing assembly is my own modified project
* [Titanium-Web-Proxy](https://github.com/rmbadmin/Titanium-Web-Proxy)
* [MetroRadiance](https://github.com/rmbadmin/MetroRadiance)
---
## License
> [GPL-3.0 License](https://github.com/rmbadmin/SteamTools/blob/develop/LICENSE)
> Open source/free software released under the GPLv3 license.
---
## Thanks to the following open source projects
## 📄 Thanks to the following Open Source Projects
* [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json)
* [MetroRadiance](https://github.com/Grabacr07/MetroRadiance)
* [MetroTrilithon](https://github.com/Grabacr07/MetroTrilithon)
@@ -98,4 +86,20 @@ Enhanced management of Steam custom covers and quick matching of downloaded app
* [WinAuth](https://github.com/winauth/winauth)
* [SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator)
* [Gameloop.Vdf](https://github.com/shravan2x/Gameloop.Vdf)
* [Costura.Fody](https://github.com/Fody/Costura)
* [Costura.Fody](https://github.com/Fody/Costura)
* [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp)
* [Nito.Comparers](https://github.com/StephenCleary/Comparers)
* [Crc32.NET](https://github.com/force-net/Crc32.NET)
* [gfoidl.Base64](https://github.com/gfoidl/Base64)
* [sqlite-net-pcl](https://github.com/praeclarum/sqlite-net)
* [Polly](https://github.com/App-vNext/Polly)
* [NLog](https://github.com/nlog/NLog)
* [NUnit](https://github.com/nunit/nunit)
* [ReactiveUI](https://github.com/reactiveui/reactiveui)
* [MessageBox.Avalonia](https://github.com/AvaloniaUtils/MessageBox.Avalonia)
* [AvaloniaUI](https://github.com/AvaloniaUI/Avalonia)
* [EntityFrameworkCore](https://github.com/dotnet/efcore)
* [ASP.NET Core](https://github.com/dotnet/aspnetcore)
* [Xamarin.Essentials](https://github.com/xamarin/essentials)
* [.NET Extensions](https://github.com/dotnet/extensions)
* [.NET Runtime](https://github.com/dotnet/runtime)
+80 -73
View File
@@ -1,10 +1,8 @@
<img src="https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/icon/LOGO1.png" alt="logo" width="130" height="130" align="left" />
<h1 align="center">Steam++ 工具箱 🧰</h1>
<h1>Steam++ 工具箱</h1>
<div align="center">
> 「Steam++」是一个包含多种Steam工具功能的工具箱,
> 此工具的大部分功能都是需要您下载安装Steam才能使用。
<br/>
「Steam++」是一个包含多种Steam工具功能的工具箱,此工具的大部分功能都是需要您下载安装Steam才能使用。
![Release Download](https://img.shields.io/github/downloads/rmbadmin/SteamTools/latest/total?style=flat-square)
[![Release Version](https://img.shields.io/github/v/release/rmbadmin/SteamTools?style=flat-square)](https://github.com/rmbadmin/SteamTools/releases/latest)
@@ -13,79 +11,72 @@
[![GitHub Fork](https://img.shields.io/github/forks/rmbadmin/SteamTools?style=flat-square)](https://github.com/rmbadmin/SteamTools/network/members)
![GitHub repo size](https://img.shields.io/github/repo-size/rmbadmin/SteamTools?style=flat-square&color=3cb371)
# Steam++
</div>
## 效果展示
-------
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/s.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/e.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/e2.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/t.png)
------
[English](./README.en.md) | 简体中文
## 核心功能
## ✨ 功能
1. 反代 Steam 的社区网页使其能正常访问
- 功能类似羽翼城大佬的 [steamcommunity_302](https://www.dogfight360.com/blog/686/)
- 使用 [Titanium-Web-Proxy](https://github.com/justcoding121/Titanium-Web-Proxy) 开源项目进行本地反代,使国内用户可以正常访问steam社区页
- 相比 **302工具** 具有更快的启动速度,以及支持简单的脚本注入(还顺便支持了Pixiv、Discord、Twitch等网站的反代支持)
2. 快速切换当前设备已记住登陆的 Steam 账号
- 该功能是读取 Steam 路径下存储的本地用户登录记录直接展示操作,可以多账号切换无需重新输入密码和令牌
3. Steam游戏的成就统计管理功能
- 功能参考 [SteamAchievementManager](https://github.com/gibbed/SteamAchievementManager) 进行二次开发,修改了游戏列表的加载和操作易用性
4. Steam本地两步身份验证器
- 功能参考 [WinAuth](https://github.com/winauth/winauth) 开发,可以使您不用启动移动版 Steam App 也能查看您的令牌
- 本地令牌交易市场报价确认
- 功能类似的软件例如:
- [WinAuth](https://github.com/winauth/winauth)
- [SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator)
5. 其他游戏工具
- 目前已有强制游戏无边框窗口化,CSGO 修复 VAC 屏蔽
### 1. 反代Steam的社区网页使其能正常访问
功能类似羽翼城大佬的[steamcommunity_302](https://www.dogfight360.com/blog/686/),使用[Titanium-Web-Proxy](https://github.com/justcoding121/Titanium-Web-Proxy)开源项目进行本地反代,使国内用户可以正常访问steam社区页,相比302工具具有更快的启动速度,以及支持简单的脚本注入。(还顺便支持了Pixiv、Discord、Twitch等网站的反代支持)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/1.png)
---
### 2. 快速切换当前PC已经记住登陆的Steam账号
## 🖥 系统要求
- Windows 10 1607+
- Windows 8.1 / Windows Server 2012 R2
- 先决条件
- Microsoft Visual C++ 2015-2019 Redistributable [64 位](https://aka.ms/vs/16/release/vc_redist.x64.exe) / [32 位](https://aka.ms/vs/16/release/vc_redist.x86.exe)
- Windows 7 SP1 [ESU](https://docs.microsoft.com/zh-cn/troubleshoot/windows-client/windows-7-eos-faq/windows-7-extended-security-updates-faq)
- 先决条件
- Microsoft Visual C++ 2015-2019 Redistributable [64 位](https://aka.ms/vs/16/release/vc_redist.x64.exe) / [32 位](https://aka.ms/vs/16/release/vc_redist.x86.exe)
- KB3063858 [64 位](https://www.microsoft.com/zh-cn/download/details.aspx?id=47442) / [32 位](https://www.microsoft.com/zh-cn/download/details.aspx?id=47409)
- ~~macOS 10.13 High Sierra 或更高版本~~
<!--
- ~~Alpine Linux 3.11+~~
- ~~CentOS 7+~~
- ~~Debian 9+~~
- ~~Fedora 32+~~
- ~~Linux Mint 18+~~
- ~~openSUSE 15+~~
- ~~Red Hat Enterprise Linux 7+~~
- ~~SUSE Enterprise Linux (SLES) 12 SP2+~~
- ~~Ubuntu 20.10, 20.04, 18.04, 16.04~~
- ~~Android 5.0+~~
- ~~iOS 8.0+~~
-->
该功能是读取Steam路径下存储的本地用户登录记录直接展示操作,可以多账号切换无需重新输入密码和令牌。
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/2.png)
---
### 3. Steam游戏的成就统计管理功能
功能参考[SteamAchievementManager](https://github.com/gibbed/SteamAchievementManager)进行二次开发,修改了游戏列表的加载和操作易用性。
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/3.png)
---
## 🌎 路线图
查看这个 [milestones](https://github.com/SteamTools-Team/SteamTools/milestones) 来了解我们下一步的开发计划,并随时提出问题。
### 4. Steam本地两步身份验证器
功能参考[WinAuth](https://github.com/winauth/winauth)开发,可以使您不用启动移动版Steam App也能查看您的令牌,功能类似的软件有[WinAuth](https://github.com/winauth/winauth)、[SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator)。
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/4.png)
---
## ⌨️ 开发环境
[Visual Studio 2019 版本 16.8 或更高版本](https://visualstudio.microsoft.com/zh-hans/vs/) 或 [Visual Studio for Mac](https://visualstudio.microsoft.com/zh-hans/vs/mac/) 或 [JetBrains Rider](https://www.jetbrains.com/rider/) 或 [Visual Studio Code](https://code.visualstudio.com/)
- 工作负载
- Web 和云
- ASP.NET 和 Web 开发
- 桌面应用和移动应用
- .NET 桌面开发
- 使用 .NET 的移动开发
- 其他工具集
- .NET Core 跨平台开发
- 单个组件
- GitHub Extension for Visual Studio
- [Visual Studio Marketplace](https://marketplace.visualstudio.com/)
- [Avalonia for Visual Studio](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio)
- [NUnit VS Templates](https://marketplace.visualstudio.com/items?itemName=NUnitDevelopers.NUnitTemplatesforVisualStudio)
### 5. 一些游戏工具
目前已有强制游戏无边框窗口化,CSGO修复VAC屏蔽。
这一块是随缘做一些我经常用或者闲着没事捣鼓的功能。
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/5.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/6.png)
---
## 预计后续添加的功能
### Steam自动挂卡
尝试用社区反代功能结合成就解锁功能来重新实现,目的是实现在软件内无需登录Steam帐户即可直接获取徽章卡片信息并开始挂卡。
### Steam皮肤设计器
挖坑画大饼,可视化编辑Steam皮肤,而且如果软件能上架Steam的话打算支持创意工坊分享设计的Steam皮肤,短期内肯定做不完。
### 插件形式加载运行ASF
以插件形式支持ASF在工具内运行并增强ASF在Windows Desktop环境下的使用。
### Steam自定义封面管理
增强Steam自定义封面的管理以及从[SteamGridDB](https://www.steamgriddb.com/)快速匹配下载应用封面。
### 更多其它游戏平台功能
### 更多非作弊游戏功能
---
## 开发环境
> 开发工具 [Visual Studio 2019](https://visualstudio.microsoft.com/)
> 开发语言 C# WPF
> .NET版本 .NET Framework4.7.2 和 .NET5
> 如果无法运行请下载安装[.NET Framework 4.7.2](https://dotnet.microsoft.com/download/dotnet-framework/net472)或[.NET 5](https://dotnet.microsoft.com/download/dotnet/5.0)。
---
## License
> [GPL-3.0 License](https://github.com/rmbadmin/SteamTools/blob/develop/LICENSE)
> 根据GPLv3许可发布的开源/免费软件。
---
## 感谢以下开源项目
## 📄 感谢以下开源项目
* [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json)
* [MetroRadiance](https://github.com/Grabacr07/MetroRadiance)
* [MetroTrilithon](https://github.com/Grabacr07/MetroTrilithon)
@@ -102,4 +93,20 @@
* [WinAuth](https://github.com/winauth/winauth)
* [SteamDesktopAuthenticator](https://github.com/Jessecar96/SteamDesktopAuthenticator)
* [Gameloop.Vdf](https://github.com/shravan2x/Gameloop.Vdf)
* [Costura.Fody](https://github.com/Fody/Costura)
* [Costura.Fody](https://github.com/Fody/Costura)
* [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp)
* [Nito.Comparers](https://github.com/StephenCleary/Comparers)
* [Crc32.NET](https://github.com/force-net/Crc32.NET)
* [gfoidl.Base64](https://github.com/gfoidl/Base64)
* [sqlite-net-pcl](https://github.com/praeclarum/sqlite-net)
* [Polly](https://github.com/App-vNext/Polly)
* [NLog](https://github.com/nlog/NLog)
* [NUnit](https://github.com/nunit/nunit)
* [ReactiveUI](https://github.com/reactiveui/reactiveui)
* [MessageBox.Avalonia](https://github.com/AvaloniaUtils/MessageBox.Avalonia)
* [AvaloniaUI](https://github.com/AvaloniaUI/Avalonia)
* [EntityFrameworkCore](https://github.com/dotnet/efcore)
* [ASP.NET Core](https://github.com/dotnet/aspnetcore)
* [Xamarin.Essentials](https://github.com/xamarin/essentials)
* [.NET Extensions](https://github.com/dotnet/extensions)
* [.NET Runtime](https://github.com/dotnet/runtime)
+165
View File
@@ -0,0 +1,165 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31005.135
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.Repositories.sqlite-net-pcl", "System.Application.Repositories.sqlite-net-pcl\System.Application.Repositories.sqlite-net-pcl.csproj", "{512B0511-41AB-444D-BAD8-E19B7D3B52CB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.ClientLib", "System.Common.ClientLib\System.Common.ClientLib.csproj", "{8B9F2E93-B31B-4BA7-920D-732695A473E6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.CoreLib", "System.Common.CoreLib\System.Common.CoreLib.csproj", "{E7468FDD-75F7-4B08-BA47-B715BBCFCC11}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.DesktopLib", "System.Common.DesktopLib\System.Common.DesktopLib.csproj", "{F37D23AA-4EA9-4E07-9B3D-F966ABED046F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.ClientLib.Droid", "System.Common.ClientLib.Droid\System.Common.ClientLib.Droid.csproj", "{7C8D906E-426B-49AB-9072-178555370F62}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.ClientLib.iOS", "System.Common.ClientLib.iOS\System.Common.ClientLib.iOS.csproj", "{F8F6973C-5831-43D1-89C2-1AD543CE7408}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0_Common", "0_Common", "{2D44BDCF-B70C-4C02-BDC8-4F1DD5066A75}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0_Shared", "0_Shared", "{D40853F1-B3C5-418A-B1D3-E0FB70B2F394}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2_Client", "2_Client", "{9AF522B5-8923-41E6-8A0A-FAE8E8F83439}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4_Repositories.Implementation", "4_Repositories.Implementation", "{DA8EFCA8-5396-48C7-8DC0-CB28A09488D9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1_SteamTools", "1_SteamTools", "{B43459A1-709B-4623-9677-AAC2FEE4FBA3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Services.CloudService.Models", "System.Application.SteamTools.Services.CloudService.Models\System.Application.SteamTools.Services.CloudService.Models.csproj", "{6CD71E14-5036-4A16-8084-4AD108D34FF7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Services.CloudService", "System.Application.SteamTools.Services.CloudService\System.Application.SteamTools.Services.CloudService.csproj", "{65DE552F-5E62-4E4F-ADCB-F19AD3DB3F78}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1_ServerApiClient", "1_ServerApiClient", "{F7AFD002-B231-4A5E-B320-56EFC3210E2C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0_Launch", "0_Launch", "{1460800B-D9ED-4147-B1E7-ABF502BEF59A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3_Test", "3_Test", "{EBC9DD0C-F7D0-4A00-B263-E69CF4E194F1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.UnitTest", "System.Common.UnitTest\System.Common.UnitTest.csproj", "{B9D4C8C7-BF96-417B-A7F8-9A67AE75A38B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Common.UnitTest.Droid", "System.Common.UnitTest.Droid\System.Common.UnitTest.Droid.csproj", "{0073A08E-6336-4C09-99F9-2E808889D715}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Common.UnitTest.Droid.App", "System.Common.UnitTest.Droid.App\System.Common.UnitTest.Droid.App.csproj", "{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools", "System.Application.SteamTools\System.Application.SteamTools.csproj", "{66B7BFD6-6D4D-421B-BA5B-915348E53708}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client", "System.Application.SteamTools.Client\System.Application.SteamTools.Client.csproj", "{0A624994-B0A9-4F1F-B7D0-97FE2929936B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop", "System.Application.SteamTools.Client.Desktop\System.Application.SteamTools.Client.Desktop.csproj", "{D1F15D97-802E-4AA3-A4A5-B890ABE32897}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop.Avalonia.App", "System.Application.SteamTools.Client.Desktop.Avalonia.App\System.Application.SteamTools.Client.Desktop.Avalonia.App.csproj", "{2FFA5BCE-B3C3-46EA-8830-838C938BF814}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B4B548C1-9AFE-4EBB-96E4-990B57C0A953}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
global.json = global.json
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop.Avalonia", "System.Application.SteamTools.Client.Desktop.Avalonia\System.Application.SteamTools.Client.Desktop.Avalonia.csproj", "{4F09B6D1-3F83-45A0-AEC8-0F243E7CD263}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{512B0511-41AB-444D-BAD8-E19B7D3B52CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{512B0511-41AB-444D-BAD8-E19B7D3B52CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{512B0511-41AB-444D-BAD8-E19B7D3B52CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{512B0511-41AB-444D-BAD8-E19B7D3B52CB}.Release|Any CPU.Build.0 = Release|Any CPU
{8B9F2E93-B31B-4BA7-920D-732695A473E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B9F2E93-B31B-4BA7-920D-732695A473E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B9F2E93-B31B-4BA7-920D-732695A473E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B9F2E93-B31B-4BA7-920D-732695A473E6}.Release|Any CPU.Build.0 = Release|Any CPU
{E7468FDD-75F7-4B08-BA47-B715BBCFCC11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7468FDD-75F7-4B08-BA47-B715BBCFCC11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7468FDD-75F7-4B08-BA47-B715BBCFCC11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7468FDD-75F7-4B08-BA47-B715BBCFCC11}.Release|Any CPU.Build.0 = Release|Any CPU
{F37D23AA-4EA9-4E07-9B3D-F966ABED046F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F37D23AA-4EA9-4E07-9B3D-F966ABED046F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F37D23AA-4EA9-4E07-9B3D-F966ABED046F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F37D23AA-4EA9-4E07-9B3D-F966ABED046F}.Release|Any CPU.Build.0 = Release|Any CPU
{7C8D906E-426B-49AB-9072-178555370F62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C8D906E-426B-49AB-9072-178555370F62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C8D906E-426B-49AB-9072-178555370F62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C8D906E-426B-49AB-9072-178555370F62}.Release|Any CPU.Build.0 = Release|Any CPU
{F8F6973C-5831-43D1-89C2-1AD543CE7408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8F6973C-5831-43D1-89C2-1AD543CE7408}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8F6973C-5831-43D1-89C2-1AD543CE7408}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8F6973C-5831-43D1-89C2-1AD543CE7408}.Release|Any CPU.Build.0 = Release|Any CPU
{6CD71E14-5036-4A16-8084-4AD108D34FF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CD71E14-5036-4A16-8084-4AD108D34FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CD71E14-5036-4A16-8084-4AD108D34FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CD71E14-5036-4A16-8084-4AD108D34FF7}.Release|Any CPU.Build.0 = Release|Any CPU
{65DE552F-5E62-4E4F-ADCB-F19AD3DB3F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{65DE552F-5E62-4E4F-ADCB-F19AD3DB3F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{65DE552F-5E62-4E4F-ADCB-F19AD3DB3F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{65DE552F-5E62-4E4F-ADCB-F19AD3DB3F78}.Release|Any CPU.Build.0 = Release|Any CPU
{B9D4C8C7-BF96-417B-A7F8-9A67AE75A38B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9D4C8C7-BF96-417B-A7F8-9A67AE75A38B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9D4C8C7-BF96-417B-A7F8-9A67AE75A38B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9D4C8C7-BF96-417B-A7F8-9A67AE75A38B}.Release|Any CPU.Build.0 = Release|Any CPU
{0073A08E-6336-4C09-99F9-2E808889D715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0073A08E-6336-4C09-99F9-2E808889D715}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0073A08E-6336-4C09-99F9-2E808889D715}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0073A08E-6336-4C09-99F9-2E808889D715}.Release|Any CPU.Build.0 = Release|Any CPU
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}.Release|Any CPU.Build.0 = Release|Any CPU
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA}.Release|Any CPU.Deploy.0 = Release|Any CPU
{66B7BFD6-6D4D-421B-BA5B-915348E53708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66B7BFD6-6D4D-421B-BA5B-915348E53708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66B7BFD6-6D4D-421B-BA5B-915348E53708}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66B7BFD6-6D4D-421B-BA5B-915348E53708}.Release|Any CPU.Build.0 = Release|Any CPU
{0A624994-B0A9-4F1F-B7D0-97FE2929936B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A624994-B0A9-4F1F-B7D0-97FE2929936B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A624994-B0A9-4F1F-B7D0-97FE2929936B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A624994-B0A9-4F1F-B7D0-97FE2929936B}.Release|Any CPU.Build.0 = Release|Any CPU
{D1F15D97-802E-4AA3-A4A5-B890ABE32897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1F15D97-802E-4AA3-A4A5-B890ABE32897}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1F15D97-802E-4AA3-A4A5-B890ABE32897}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1F15D97-802E-4AA3-A4A5-B890ABE32897}.Release|Any CPU.Build.0 = Release|Any CPU
{2FFA5BCE-B3C3-46EA-8830-838C938BF814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2FFA5BCE-B3C3-46EA-8830-838C938BF814}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FFA5BCE-B3C3-46EA-8830-838C938BF814}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FFA5BCE-B3C3-46EA-8830-838C938BF814}.Release|Any CPU.Build.0 = Release|Any CPU
{4F09B6D1-3F83-45A0-AEC8-0F243E7CD263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F09B6D1-3F83-45A0-AEC8-0F243E7CD263}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F09B6D1-3F83-45A0-AEC8-0F243E7CD263}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F09B6D1-3F83-45A0-AEC8-0F243E7CD263}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{512B0511-41AB-444D-BAD8-E19B7D3B52CB} = {DA8EFCA8-5396-48C7-8DC0-CB28A09488D9}
{8B9F2E93-B31B-4BA7-920D-732695A473E6} = {9AF522B5-8923-41E6-8A0A-FAE8E8F83439}
{E7468FDD-75F7-4B08-BA47-B715BBCFCC11} = {D40853F1-B3C5-418A-B1D3-E0FB70B2F394}
{F37D23AA-4EA9-4E07-9B3D-F966ABED046F} = {9AF522B5-8923-41E6-8A0A-FAE8E8F83439}
{7C8D906E-426B-49AB-9072-178555370F62} = {9AF522B5-8923-41E6-8A0A-FAE8E8F83439}
{F8F6973C-5831-43D1-89C2-1AD543CE7408} = {9AF522B5-8923-41E6-8A0A-FAE8E8F83439}
{D40853F1-B3C5-418A-B1D3-E0FB70B2F394} = {2D44BDCF-B70C-4C02-BDC8-4F1DD5066A75}
{9AF522B5-8923-41E6-8A0A-FAE8E8F83439} = {2D44BDCF-B70C-4C02-BDC8-4F1DD5066A75}
{DA8EFCA8-5396-48C7-8DC0-CB28A09488D9} = {2D44BDCF-B70C-4C02-BDC8-4F1DD5066A75}
{6CD71E14-5036-4A16-8084-4AD108D34FF7} = {F7AFD002-B231-4A5E-B320-56EFC3210E2C}
{65DE552F-5E62-4E4F-ADCB-F19AD3DB3F78} = {F7AFD002-B231-4A5E-B320-56EFC3210E2C}
{F7AFD002-B231-4A5E-B320-56EFC3210E2C} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{1460800B-D9ED-4147-B1E7-ABF502BEF59A} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{EBC9DD0C-F7D0-4A00-B263-E69CF4E194F1} = {2D44BDCF-B70C-4C02-BDC8-4F1DD5066A75}
{B9D4C8C7-BF96-417B-A7F8-9A67AE75A38B} = {EBC9DD0C-F7D0-4A00-B263-E69CF4E194F1}
{0073A08E-6336-4C09-99F9-2E808889D715} = {EBC9DD0C-F7D0-4A00-B263-E69CF4E194F1}
{AD7B78B0-AE51-4FB7-9B11-4E22A2B538CA} = {EBC9DD0C-F7D0-4A00-B263-E69CF4E194F1}
{66B7BFD6-6D4D-421B-BA5B-915348E53708} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{0A624994-B0A9-4F1F-B7D0-97FE2929936B} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{D1F15D97-802E-4AA3-A4A5-B890ABE32897} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{2FFA5BCE-B3C3-46EA-8830-838C938BF814} = {1460800B-D9ED-4147-B1E7-ABF502BEF59A}
{4F09B6D1-3F83-45A0-AEC8-0F243E7CD263} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3833E7C5-69BC-4CB3-92CE-A8854AA35D8A}
EndGlobalSection
EndGlobal
@@ -0,0 +1,187 @@
using Polly;
using SQLite;
using System.Application.Entities;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace System.Application.Repositories
{
/// <summary>
/// 由 sqlite-net-pcl 实现的仓储层
/// </summary>
public abstract class Repository : IRepository
{
#region https://codetraveler.io/2019/11/26/efficiently-initializing-sqlite-database/
static string? mDataBaseDirectory;
/// <summary>
/// 获取或设定数据库目录,默认值为 <see cref="IOPath.AppDataDirectory"/>
/// </summary>
public static string DataBaseDirectory
{
get
{
if (string.IsNullOrWhiteSpace(mDataBaseDirectory)) mDataBaseDirectory = IOPath.AppDataDirectory;
return mDataBaseDirectory;
}
set
{
mDataBaseDirectory = value;
}
}
static SQLiteAsyncConnection GetConnection()
{
var dbPath = DataBaseDirectory;
IOPath.DirCreateByNotExists(dbPath);
dbPath = Path.Combine(dbPath, "application.dbf");
return new SQLiteAsyncConnection(dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache);
}
static readonly Lazy<SQLiteAsyncConnection> dbConnection = new Lazy<SQLiteAsyncConnection>(GetConnection);
static SQLiteAsyncConnection DbConnection => dbConnection.Value;
protected static async ValueTask<SQLiteAsyncConnection> GetDbConnection<T>()
{
if (!DbConnection.TableMappings.Any(x => x.MappedType == typeof(T)))
{
// On sqlite-net v1.6.0+, enabling write-ahead logging allows for faster database execution
await DbConnection.EnableWriteAheadLoggingAsync().ConfigureAwait(false);
await DbConnection.CreateTablesAsync(CreateFlags.None, typeof(T)).ConfigureAwait(false);
}
return DbConnection;
}
protected static Task<T> AttemptAndRetry<T>(Func<Task<T>> action, int numRetries = 10)
{
return Policy.Handle<SQLiteException>().WaitAndRetryAsync(numRetries, pollyRetryAttempt).ExecuteAsync(action);
static TimeSpan pollyRetryAttempt(int attemptNumber) => TimeSpan.FromMilliseconds(Math.Pow(2, attemptNumber));
}
#endregion
}
/// <inheritdoc cref="Repository"/>
public abstract class Repository<TEntity> : Repository, IRepository<TEntity> where TEntity : class, new()
{
protected static ValueTask<SQLiteAsyncConnection> GetDbConnection() => GetDbConnection<TEntity>();
#region 增(Insert Funs) 立即执行并返回受影响的行数
public virtual async Task<int> InsertAsync(TEntity entity)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.InsertAsync(entity)).ConfigureAwait(false);
}
public virtual async Task<int> InsertRangeAsync(IEnumerable<TEntity> entities)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.InsertAllAsync(entities)).ConfigureAwait(false);
}
#endregion
#region 删(Delete Funs) 立即执行并返回受影响的行数
public virtual async Task<int> DeleteAsync(TEntity entity)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.DeleteAsync(entity)).ConfigureAwait(false);
}
#endregion
#region 改(Update Funs) 立即执行并返回受影响的行数
public virtual async Task<int> UpdateAsync(TEntity entity)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.UpdateAsync(entity)).ConfigureAwait(false);
}
public virtual async Task<int> UpdateRangeAsync(IEnumerable<TEntity> entities)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.UpdateAllAsync(entities)).ConfigureAwait(false);
}
#endregion
#region 查(通用查询)
public virtual async Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.FindAsync(predicate)).ConfigureAwait(false);
}
#endregion
}
/// <inheritdoc cref="IRepository"/>
public abstract class Repository<TEntity, TPrimaryKey> : Repository<TEntity>, IRepository<TEntity, TPrimaryKey>
where TEntity : class, IEntity<TPrimaryKey>, new()
where TPrimaryKey : IEquatable<TPrimaryKey>
{
object IGetPrimaryKey<TEntity>.GetPrimaryKey(TEntity entity) => GetPrimaryKey(entity);
protected TPrimaryKey GetPrimaryKey(TEntity entity)
{
IGetPrimaryKey<TEntity, TPrimaryKey> getPrimaryKey = this;
return getPrimaryKey.GetPrimaryKey(entity);
}
#region 删(Delete Funs) 立即执行并返回受影响的行数
public override Task<int> DeleteAsync(TEntity entity)
{
var primaryKey = GetPrimaryKey(entity);
return DeleteAsync(primaryKey);
}
public virtual async Task<int> DeleteAsync(TPrimaryKey primaryKey)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.DeleteAsync<TEntity>(primaryKey)).ConfigureAwait(false);
}
#endregion
#region 查(通用查询)
public virtual async ValueTask<TEntity?> FindAsync(TPrimaryKey primaryKey)
{
if (IRepository<TEntity, TPrimaryKey>.IsDefault(primaryKey)) return default;
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(() => dbConnection.FindAsync<TEntity>(primaryKey)).ConfigureAwait(false);
}
public virtual async ValueTask<bool> ExistAsync(TPrimaryKey primaryKey)
{
if (IRepository<TEntity, TPrimaryKey>.IsDefault(primaryKey)) return false;
var dbConnection = await GetDbConnection().ConfigureAwait(false);
var count = await AttemptAndRetry(CountAsync).ConfigureAwait(false);
return count > 0;
Task<int> CountAsync() => dbConnection.Table<TEntity>().CountAsync(IRepository<TEntity, TPrimaryKey>.LambdaEqualId(primaryKey));
}
#endregion
#region 增或改(InsertOrUpdate Funs) 立即执行并返回受影响的行数
public virtual async Task<(int rowCount, DbRowExecResult result)> InsertOrUpdateAsync(TEntity entity)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
var rowCount = await AttemptAndRetry(() => dbConnection.InsertOrReplaceAsync(entity)).ConfigureAwait(false);
return (rowCount, DbRowExecResult.InsertOrUpdate);
}
#endregion
}
}
@@ -0,0 +1,64 @@
using SQLite;
using System.Application.Models;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace System
{
public static class AsyncTableQueryExtensions
{
#region WhereOr
/// <inheritdoc cref="QueryableExtensions.WhereOr{T}(IReadOnlyList{Expression{Func{T, bool}}})"/>
public static AsyncTableQuery<T> WhereOr<T>(this AsyncTableQuery<T> source, IReadOnlyList<Expression<Func<T, bool>>> predicates) where T : new()
{
var predicate = ExpressionHelper.WhereOr(predicates);
return source.Where(predicate);
}
/// <inheritdoc cref="QueryableExtensions.WhereOr{T}(IReadOnlyList{Expression{Func{T, bool}}})"/>
public static AsyncTableQuery<T> WhereOr<T>(this AsyncTableQuery<T> source, IEnumerable<Expression<Func<T, bool>>> predicates) where T : new()
=> source.WhereOr(predicates.ToArray());
/// <inheritdoc cref="QueryableExtensions.WhereOr{T}(IReadOnlyList{Expression{Func{T, bool}}})"/>
public static AsyncTableQuery<T> WhereOr<T>(this AsyncTableQuery<T> source, params Expression<Func<T, bool>>[] predicates) where T : new()
{
IReadOnlyList<Expression<Func<T, bool>>> _predicates = predicates;
return source.WhereOr(_predicates);
}
#endregion
#region Paging
/// <summary>
/// 分页查询,调用此方法前必须进行排序
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="source"></param>
/// <param name="current"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
public static async Task<PagedModel<TEntity>> PagingAsync<TEntity>(
this AsyncTableQuery<TEntity> source,
int current = IPagedModel.DefaultCurrent,
int pageSize = IPagedModel.DefaultPageSize) where TEntity : new()
{
var skipCount = (pageSize - 1) * pageSize;
var total = await source.CountAsync();
var dataSource = await source.Skip(skipCount).Take(pageSize).ToListAsync();
var pagedModel = new PagedModel<TEntity>
{
Current = current,
PageSize = pageSize,
Total = total,
DataSource = dataSource,
};
return pagedModel;
}
#endregion
}
}
@@ -0,0 +1,100 @@
using System.Application.Columns;
using System.Runtime.CompilerServices;
namespace System
{
/// <summary>
/// SQL语句字符串常量,使用字符串拼接时应注意防止 SQL注入攻击
/// <para>EFCore 可采用 <see cref="FormattableString"/> 参数化传入参数</para>
/// </summary>
public static partial class SQLStrings
{
/// <summary>
/// "select * from "
/// <para>查询所有列</para>
/// </summary>
public const string SelectALLFrom = "select * from ";
/// <summary>
/// "select count(Id) from "
/// <para>查询总数,仅Id列</para>
/// </summary>
public const string SelectCountFrom = "select count(Id) from ";
/// <summary>
/// "select count({0}) from "
/// <para>查询总数,仅某一列</para>
/// <para>注意:参数仅使用列名固定值,不可传递变量,防止SQL注入攻击</para>
/// </summary>
public const string SelectCountFrom_0 = "select count({0}) from ";
/// <summary>
/// " order by CreationTime desc"
/// <para>根据 创建时间 倒序</para>
/// </summary>
public const string OrderByCreationTimeDescending = " order by CreationTime desc";
/// <summary>
/// " order by ReceiveTime, CreationTime desc"
/// <para>根据 接收时间, 创建时间 倒序</para>
/// </summary>
public const string OrderByReceiveTimeThenByCreationTimeDescending = " order by ReceiveTime, CreationTime desc";
/// <summary>
/// " order by {0} desc"
/// <para>根据 某一列 倒序</para>
/// <para>注意:参数仅使用列名固定值,不可传递变量,防止SQL注入攻击</para>
/// </summary>
public const string OrderBy_Descending_0 = " order by {0} desc";
/// <summary>
/// "delete from "
/// </summary>
public const string DeleteFrom = "delete from ";
/// <summary>
/// " where Id = {0}"
/// <para>注意:参数仅使用列名固定值,不可传递变量,防止SQL注入攻击</para>
/// </summary>
public const string WhereIdEqual_0 = " where Id = {0}";
/// <summary>
/// "delete from {tableName} where Id = {0}"
/// </summary>
/// <param name="tableName"></param>
/// <param name="id"></param>
/// <returns></returns>
public static FormattableString DeleteFromTableNameWhereIdEqual(string tableName, object id)
{
var sql = DeleteFrom + tableName + WhereIdEqual_0;
return FormattableStringFactory.Create(sql, id);
}
/// <summary>
/// "update from {tableName} set IsSoftDeleted = 1 where Id = {0}"
/// </summary>
/// <param name="tableName"></param>
/// <param name="id"></param>
/// <returns></returns>
public static FormattableString SoftDeleteFromTableNameWhereIdEqual(string tableName, object id)
{
var sql = UpdateFrom + tableName + " set " + nameof(ISoftDeleted.SoftDeleted) + " = 1" + WhereIdEqual_0;
return FormattableStringFactory.Create(sql, id);
}
/// <summary>
/// "update from "
/// </summary>
public const string UpdateFrom = "update from ";
/// <summary>
/// " where Id in ("
/// </summary>
public const string WhereInIds_ = " where Id in (";
/// <summary>
/// 右括号
/// </summary>
public const char RightBracket = ')';
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>System</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
<Compile Include="..\System.Common.CoreLib\Properties\InternalsVisibleTo.cs">
<Link>Properties\InternalsVisibleTo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Common.CoreLib\System.Common.CoreLib.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="sqlite-net-pcl" Version="1.7.335" />
<PackageReference Include="Polly" Version="7.2.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:System.Application.UI"
x:Class="System.Application.UI.App">
<Application.DataTemplates>
<ui:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Dark" />
<StyleInclude Source="/Styles/Styles.xaml" />
<StyleInclude Source="/Styles/SideBar.xaml" />
<!--<StyleInclude Source="avares://AvaloniaEdit/AvaloniaEdit.xaml"/>-->
<Style Selector="Window">
<Setter Property="Icon" Value="/Assets/Icon.ico"/>
</Style>
</Application.Styles>
</Application>
@@ -0,0 +1,36 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using System.Application.UI.ViewModels;
using System.Application.UI.Views;
using AvaloniaApplication = Avalonia.Application;
namespace System.Application.UI
{
public class App : AvaloniaApplication
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// ÔÚUIÔ¤ÀÀÖУ¬ApplicationLifetime Ϊ null
ViewModelBase.IsInDesignMode = ApplicationLifetime == null;
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = DataContext = DI.Get<MainWindowViewModel>(),
};
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
}
base.OnFrameworkInitializationCompleted();
}
public static async void SetClipboardText(string s) => await Current.Clipboard.SetTextAsync(s);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

@@ -0,0 +1,46 @@
using Avalonia.Controls.Primitives;
using Avalonia.Platform;
using Avalonia.Styling;
using System;
// ReSharper disable once CheckNamespace
namespace Avalonia.Controls
{
public class FluentWindow : Window, IStyleable
{
Type IStyleable.StyleKey => typeof(Window);
public FluentWindow()
{
ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaTitleBarHeightHint = -1;
TransparencyLevelHint = WindowTransparencyLevel.AcrylicBlur;
this.GetObservable(WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
});
this.GetObservable(IsExtendedIntoWindowDecorationsProperty)
.Subscribe(x =>
{
if (!x)
{
SystemDecorations = SystemDecorations.Full;
TransparencyLevelHint = WindowTransparencyLevel.Blur;
}
});
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
ExtendClientAreaChromeHints =
ExtendClientAreaChromeHints.PreferSystemChrome |
ExtendClientAreaChromeHints.OSXThickTitleBar;
}
}
}
@@ -0,0 +1,36 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:System.Application.UI.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="System.Application.UI.Pages.SettingsPage">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<StackPanel HorizontalAlignment="Stretch" Margin="{StaticResource PageMargin}" Background="Transparent" Spacing="20">
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Stretch"
Spacing="16">
<TextBlock FontSize="18" Text="{Binding Path=SettingsPage.Settings_General}"/>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=SettingsPage.Language}" Margin="0 0 0 20"/>
<ComboBox HorizontalAlignment="Stretch"
SelectedIndex="{Binding Path=SettingsPage.CurrentLanguageIndex}"
Items="{Binding Path=SettingsPage.Languages}" />
</StackPanel>
<Border Background="{DynamicResource SystemControlHighlightBaseLowBrush}" BorderThickness="1" Margin="15"/>
</StackPanel>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Stretch"
Spacing="16"
Grid.Row="1">
<TextBlock FontSize="18" Text="{Binding Path=SettingsPage.Settings_Auth}"/>
<StackPanel Orientation="Vertical">
</StackPanel>
<Border Background="{DynamicResource SystemControlHighlightBaseLowBrush}" BorderThickness="1" Margin="15"/>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>
@@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Application.UI.ViewModels;
namespace System.Application.UI.Pages
{
public class SettingsPage : UserControl
{
public SettingsPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,44 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Application.UI.ViewModels;
namespace System.Application.UI
{
static 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]
static void Main(string[] args)
{
ModelValidatorProvider.Init();
DI.Init(ConfigureServices);
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new SkiaOptions { MaxGpuResourceSizeBytes = 8096000 })
.With(new Win32PlatformOptions { AllowEglInitialization = true })
.LogToTrace()
.UseReactiveUI();
static void ConfigureServices(IServiceCollection services)
{
services.AddLogging(l => l.AddProvider(NullLoggerProvider.Instance));
services.AddDesktopPlatformService();
services.TryAddModelValidator();
services.AddLocalizationService();
services.AddMainThreadPlatformService();
services.AddViewModelCollectionService();
services.AddViewModel<MainWindowViewModel>();
services.AddViewModel<SettingsPageViewModel>();
}
}
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>True</PublishSingleFile>
<PublishTrimmed>True</PublishTrimmed>
<TrimMode>link</TrimMode>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
</Project>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>True</PublishSingleFile>
<PublishTrimmed>True</PublishTrimmed>
<TrimMode>link</TrimMode>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
</Project>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>True</PublishSingleFile>
<PublishTrimmed>True</PublishTrimmed>
<TrimMode>link</TrimMode>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
</Project>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。n
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
<requestedExecutionLevel level="highestAvailable" uiAccess="false"/>
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则删除此
元素。
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
Windows 版本的列表。取消评论适当的元素,
Windows 将自动选择最兼容的环境。 -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!--Windows 7-->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!--Windows 8-->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!--Windows 8.1-->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!--Windows 10-->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<!-- 指示该应用程序可以感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
选择加入。选择加入此设置的 Windows 窗体应用程序(目标设定为 .NET Framework 4.6 )还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。-->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
-->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
</windowsSettings>
</application>
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"/>
</dependentAssembly>
</dependency>
-->
</assembly>
@@ -0,0 +1,74 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<TabControl Classes="sidebar">
<TabItem Header="Item1"/>
<TabItem Header="Item2"/>
</TabControl>
</Border>
</Design.PreviewWith>
<Style Selector="TabControl.sidebar">
<Setter Property="TabStripPlacement" Value="Left"/>
<Setter Property="Padding" Value="0 0 0 0"/>
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="Template">
<ControlTemplate>
<Border
Margin="{TemplateBinding Margin}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<ScrollViewer Width="240" Margin="0 30 0 0"
Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
Background="{TemplateBinding Background}"
DockPanel.Dock="Left">
<ItemsPresenter
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentControl Content="{TemplateBinding Tag}" HorizontalContentAlignment="Right" DockPanel.Dock="Bottom"/>
<ScrollViewer Background="{x:Null}"
HorizontalScrollBarVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem.(ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedItem.(ScrollViewer.VerticalScrollBarVisibility)}">
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}">
</ContentPresenter>
</ScrollViewer>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabControl.sidebar > TabItem">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Width" Value="240" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)" Value="Auto"/>
<Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Auto"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected /template/ Border#PART_SelectedPipe">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected">
<Setter Property="Background" Value="#1fffffff"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="#3fffffff"/>
</Style>
</Styles>
@@ -0,0 +1,18 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="5">
<TextBlock Text="Code Font Family" FontFamily="{StaticResource CodeFontFamily}" />
</StackPanel>
</Border>
</Design.PreviewWith>
<Styles.Resources>
<Thickness x:Key="PageMargin">40 0 40 0</Thickness>
<!--<FontFamily x:Key="CodeFontFamily">avares://Steam++/Assets/Fonts#Source Code Pro</FontFamily>-->
</Styles.Resources>
<Style Selector="TextBlock.code">
<!--<Setter Property="FontFamily" Value="{StaticResource CodeFontFamily}" />-->
</Style>
</Styles>
@@ -0,0 +1,66 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<SignAssembly>false</SignAssembly>
<!--(SignAssembly)对此程序集进行签名会导致编译失败(CompileAvaloniaXamlTask)-->
<RootNamespace>System.Application.UI</RootNamespace>
<AssemblyName>Steam++</AssemblyName>
<ApplicationIcon>Assets\Icon.ico</ApplicationIcon>
<ApplicationManifest>Properties\app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop.Avalonia\System.Application.SteamTools.Client.Desktop.Avalonia.csproj" />
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop\System.Application.SteamTools.Client.Desktop.csproj" />
<ProjectReference Include="..\System.Common.CoreLib\System.Common.CoreLib.csproj" />
<ProjectReference Include="..\System.Common.DesktopLib\System.Common.DesktopLib.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0" />
<PackageReference Include="MessageBox.Avalonia" Version="1.0.5">
<Aliases>MessageBox_Avalonia</Aliases>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Remove="Styles\SideBar.axaml" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Styles\SideBar.axaml" />
</ItemGroup>
<ItemGroup>
<None Include="..\Resources\AppIcon\VisualElements_150.png">
<Link>Assets\VisualElements_150.png</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\Resources\AppIcon\VisualElements_70.png">
<Link>Assets\VisualElements_70.png</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\Resources\AppIcon\visualelementsmanifest.xml">
<Link>steam++.visualelementsmanifest.xml</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System.Application.UI.ViewModels;
namespace System.Application.UI
{
public class ViewLocator : IDataTemplate
{
#pragma warning disable CA1822 // 将成员标记为 static
public bool SupportsRecycling => false;
#pragma warning restore CA1822 // 将成员标记为 static
public IControl Build(object data)
{
var name = data.GetType().FullName?.Replace("ViewModel", "View");
var type = name == null ? null : Type.GetType(name);
if (type != null)
{
var view = (Control?)Activator.CreateInstance(type);
if (view != null)
{
return view;
}
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}
@@ -0,0 +1,100 @@
using ReactiveUI;
using System.Properties;
namespace System.Application.UI.ViewModels
{
public class MainWindowViewModel : WindowViewModel
{
bool mTopmost;
public bool Topmost
{
get => mTopmost;
set => this.RaiseAndSetIfChanged(ref mTopmost, value);
}
public MainWindowViewModel() : base()
{
Title = ThisAssembly.AssemblyTrademark;
SettingsPage = GetViewModel<SettingsPageViewModel>();
}
#region ResStrings
string mWelcome = string.Empty;
[ResString]
public string Welcome
{
get => mWelcome;
set => this.RaiseAndSetIfChanged(ref mWelcome, value);
}
string mCommunityFix = string.Empty;
[ResString]
public string CommunityFix
{
get => mCommunityFix;
set => this.RaiseAndSetIfChanged(ref mCommunityFix, value);
}
string mUserFastChange = string.Empty;
[ResString]
public string UserFastChange
{
get => mUserFastChange;
set => this.RaiseAndSetIfChanged(ref mUserFastChange, value);
}
string mSteamApps = string.Empty;
[ResString]
public string SteamApps
{
get => mSteamApps;
set => this.RaiseAndSetIfChanged(ref mSteamApps, value);
}
string mSteamAuth = string.Empty;
[ResString]
public string SteamAuth
{
get => mSteamAuth;
set => this.RaiseAndSetIfChanged(ref mSteamAuth, value);
}
string mGameRelated = string.Empty;
[ResString]
public string GameRelated
{
get => mGameRelated;
set => this.RaiseAndSetIfChanged(ref mGameRelated, value);
}
string mSettings = string.Empty;
[ResString]
public string Settings
{
get => mSettings;
set => this.RaiseAndSetIfChanged(ref mSettings, value);
}
string mAbout = string.Empty;
[ResString]
public string About
{
get => mAbout;
set => this.RaiseAndSetIfChanged(ref mAbout, value);
}
#endregion
public SettingsPageViewModel SettingsPage { get; }
}
}
@@ -0,0 +1,76 @@
using Avalonia.Controls;
using ReactiveUI;
using System.Application.Services;
using System.Collections.Generic;
using System.Linq;
namespace System.Application.UI.ViewModels
{
public class SettingsPageViewModel : ViewModelBase, ILocalizationViewModel
{
#region ResStrings
string mSettings_General = string.Empty;
[ResString]
public string Settings_General
{
get => mSettings_General;
set => this.RaiseAndSetIfChanged(ref mSettings_General, value);
}
string mSettings_Auth = string.Empty;
[ResString]
public string Settings_Auth
{
get => mSettings_Auth;
set => this.RaiseAndSetIfChanged(ref mSettings_Auth, value);
}
string mLanguage = string.Empty;
[ResString]
public string Language
{
get => mLanguage;
set => this.RaiseAndSetIfChanged(ref mLanguage, value);
}
#endregion
public SettingsPageViewModel()
{
Languages = ILocalizationService.Languages.Select(Convert).ToList();
}
int mCurrentLanguageIndex;
public int CurrentLanguageIndex
{
get => mCurrentLanguageIndex;
set
{
this.RaiseAndSetIfChanged(ref mCurrentLanguageIndex, value);
if (!IsInDesignMode)
{
var localizationService = DI.Get<ILocalizationService>();
var cultureName = Languages[value].Tag?.ToString();
if (string.IsNullOrWhiteSpace(cultureName))
{
cultureName = localizationService.DefaultCurrentUICulture?.Name;
}
if (!string.IsNullOrWhiteSpace(cultureName))
{
localizationService.ChangeLanguage(cultureName);
}
}
}
}
public List<ComboBoxItem> Languages { get; }
static ComboBoxItem Convert(KeyValuePair<string, string> pair)
=> new ComboBoxItem { Tag = pair.Key, Content = pair.Value };
}
}
@@ -0,0 +1,54 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:System.Application.UI.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1350" d:DesignHeight="700"
xmlns:pages="clr-namespace:System.Application.UI.Pages"
x:Class="System.Application.UI.Views.MainView"
FontSize="14"
Foreground="{StaticResource SystemBaseHighColor}">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<Panel>
<DockPanel HorizontalAlignment="Stretch">
<ExperimentalAcrylicBorder DockPanel.Dock="Left" Width="240">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial TintColor="Black" MaterialOpacity="0.85" TintOpacity="1"/>
</ExperimentalAcrylicBorder.Material>
<!--<AutoCompleteBox HorizontalAlignment="Stretch" Margin="20 40 20 0" Watermark="Search" Items="{Binding SearchItems}" />-->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Height="110" VerticalAlignment="Top">
<Image Source="/Assets/Icon.ico" Height="64" Width="64" VerticalAlignment="Center" Margin="-8,0,0,0"/>
<Label Content="Steam++" VerticalContentAlignment="Center" FontSize="20" Margin="8,0,0,0"/>
</StackPanel>
</ExperimentalAcrylicBorder>
<ExperimentalAcrylicBorder IsHitTestVisible="False">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial TintColor="#222222" MaterialOpacity="0.85" TintOpacity="1"/>
</ExperimentalAcrylicBorder.Material>
<TextBlock Text="{Binding #TabControl.SelectedItem.Header}" Margin="40 20" FontSize="32" FontWeight="Light"/>
</ExperimentalAcrylicBorder>
</DockPanel>
<TabControl x:Name="TabControl" Classes="sidebar" Margin="0 40 0 20">
<TabItem Header="{Binding Path=Welcome}">
</TabItem>
<TabItem Header="{Binding Path=CommunityFix}">
</TabItem>
<TabItem Header="{Binding Path=UserFastChange}">
</TabItem>
<TabItem Header="{Binding Path=SteamAuth}">
</TabItem>
<TabItem Header="{Binding Path=SteamAuth}">
</TabItem>
<TabItem Header="{Binding Path=GameRelated}">
</TabItem>
<TabItem Header="{Binding Path=Settings}">
<pages:SettingsPage />
</TabItem>
<TabItem Header="{Binding Path=About}">
</TabItem>
</TabControl>
</Panel>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace System.Application.UI.Views
{
public class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,30 @@
<FluentWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:System.Application.UI.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="1350" Width="1400" MinWidth="800"
d:DesignHeight="700" Height="800" MinHeight="600"
xmlns:pages="clr-namespace:System.Application.UI.Pages"
xmlns:local="clr-namespace:System.Application.UI.Views"
x:Class="System.Application.UI.Views.MainWindow"
x:Name="MainWindow"
WindowStartupLocation="CenterScreen"
Background="{x:Null}"
Title="{Binding Title}"
Topmost="{Binding Topmost}">
<FluentWindow.Styles>
<Style Selector="TitleBar:fullscreen">
<Setter Property="Background" Value="#7f000000" />
</Style>
</FluentWindow.Styles>
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<Panel Margin="{Binding #MainWindow.OffScreenMargin}">
<local:MainView />
</Panel>
</FluentWindow>
@@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Application.UI.ViewModels;
namespace System.Application.UI.Views
{
public class MainWindow : FluentWindow
{
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,15 @@
using System.Application.Services;
using System.Application.Services.Implementation;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMainThreadPlatformService(this IServiceCollection services)
{
services.AddSingleton<IMainThreadPlatformService, MainThreadPlatformServiceImpl>();
return services;
}
}
}
@@ -0,0 +1,11 @@
using Avalonia.Threading;
namespace System.Application.Services.Implementation
{
internal sealed class MainThreadPlatformServiceImpl : IMainThreadPlatformService
{
public bool PlatformIsMainThread => Dispatcher.UIThread.CheckAccess();
public void PlatformBeginInvokeOnMainThread(Action action) => Dispatcher.UIThread.Post(action);
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>System.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop\System.Application.SteamTools.Client.Desktop.csproj"/>
<ProjectReference Include="..\System.Common.CoreLib\System.Common.CoreLib.csproj"/>
<ProjectReference Include="..\System.Common.DesktopLib\System.Common.DesktopLib.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MessageBox.Avalonia" Version="1.0.5">
<Aliases>MessageBox_Avalonia</Aliases>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,133 @@
using System.Threading.Tasks;
using static System.Application.Services.IMainThreadPlatformService;
namespace System.Application
{
/// <summary>
/// 适用于桌面端的主线程帮助类,参考 Xamarin.Essentials.MainThread
/// <para><see cref="https://github.com/xamarin/Essentials/blob/main/Xamarin.Essentials/MainThread/MainThread.shared.cs"/></para>
/// </summary>
public static class MainThreadDesktop
{
public static bool IsMainThread => Instance.PlatformIsMainThread;
public static void BeginInvokeOnMainThread(Action action)
{
if (IsMainThread)
{
action();
}
else
{
Instance.PlatformBeginInvokeOnMainThread(action);
}
}
public static Task InvokeOnMainThreadAsync(Action action)
{
if (IsMainThread)
{
action();
#if NETSTANDARD1_0
return Task.FromResult(true);
#else
return Task.CompletedTask;
#endif
}
var tcs = new TaskCompletionSource<bool>();
BeginInvokeOnMainThread(() =>
{
try
{
action();
tcs.TrySetResult(true);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
public static Task<T> InvokeOnMainThreadAsync<T>(Func<T> func)
{
if (IsMainThread)
{
return Task.FromResult(func());
}
var tcs = new TaskCompletionSource<T>();
BeginInvokeOnMainThread(() =>
{
try
{
var result = func();
tcs.TrySetResult(result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
public static Task InvokeOnMainThreadAsync(Func<Task> funcTask)
{
if (IsMainThread)
{
return funcTask();
}
var tcs = new TaskCompletionSource<object?>();
BeginInvokeOnMainThread(
async () =>
{
try
{
await funcTask().ConfigureAwait(false);
tcs.SetResult(null);
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
public static Task<T> InvokeOnMainThreadAsync<T>(Func<Task<T>> funcTask)
{
if (IsMainThread)
{
return funcTask();
}
var tcs = new TaskCompletionSource<T>();
BeginInvokeOnMainThread(
async () =>
{
try
{
var ret = await funcTask().ConfigureAwait(false);
tcs.SetResult(ret);
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
}
}
@@ -0,0 +1,45 @@
using System.Application.Services;
using System.Application.Services.Implementation;
using System.Application.UI.ViewModels;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加本地化、多语言服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddLocalizationService(this IServiceCollection services)
{
services.AddSingleton<ILocalizationService, LocalizationServiceImpl>();
return services;
}
/// <summary>
/// 添加视图模型组服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddViewModelCollectionService(this IServiceCollection services)
{
services.AddSingleton(new ViewModelCollectionServiceImpl(services));
services.AddSingleton<IViewModelCollectionService>(s => s.GetRequiredService<ViewModelCollectionServiceImpl>());
return services;
}
/// <summary>
/// 添加视图模型
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddViewModel<TViewModel>(this IServiceCollection services) where TViewModel : ViewModelBase
{
services.AddSingleton<TViewModel>();
return services;
}
}
}
@@ -0,0 +1,39 @@
using System.Application.UI.ViewModels;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace System.Application.Services
{
/// <summary>
/// 本地化、多语言服务
/// </summary>
public interface ILocalizationService
{
CultureInfo DefaultCurrentUICulture { get; }
/// <summary>
/// 更改语言
/// </summary>
/// <param name="cultureName"></param>
void ChangeLanguage(string cultureName);
/// <summary>
/// 绑定资源字符串的视图模型组
/// </summary>
ILocalizationViewModel[] ViewModels { get; }
public static IList<KeyValuePair<string, string>> Languages;
static ILocalizationService()
{
Languages = new Dictionary<string, string>()
{
{ "", "Auto" },
{ "en", "English" },
{ "zh-Hans", "Chinese(Simplified)" },
{ "zh-Hant", "Chinese(Traditional)" },
}.ToList();
}
}
}
@@ -0,0 +1,14 @@
namespace System.Application.Services
{
/// <summary>
/// 由平台实现的主线程帮助类
/// </summary>
public interface IMainThreadPlatformService
{
bool PlatformIsMainThread { get; }
void PlatformBeginInvokeOnMainThread(Action action);
public static IMainThreadPlatformService Instance => DI.Get<IMainThreadPlatformService>();
}
}
@@ -0,0 +1,23 @@
using System.Application.UI.ViewModels;
using System.Collections.Generic;
namespace System.Application.Services
{
/// <summary>
/// 视图模型组服务
/// </summary>
public interface IViewModelCollectionService
{
/// <summary>
/// 获取所有的视图模型
/// </summary>
IEnumerable<ViewModelBase> ViewModels { get; }
/// <summary>
/// 根据类型过滤获取视图模型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
IEnumerable<T> GetViewModels<T>();
}
}
@@ -0,0 +1,64 @@
using System.Application.UI.ViewModels;
using System.Globalization;
using System.Linq;
namespace System.Application.Services.Implementation
{
internal sealed class LocalizationServiceImpl : ILocalizationService
{
readonly IViewModelCollectionService vmss;
public LocalizationServiceImpl(IViewModelCollectionService vmss)
{
this.vmss = vmss;
DefaultCurrentUICulture = CultureInfo.CurrentUICulture;
}
public CultureInfo DefaultCurrentUICulture { get; }
ILocalizationViewModel[]? viewModels;
public ILocalizationViewModel[] ViewModels
{
get
{
if (viewModels == null)
{
viewModels = vmss.GetViewModels<ILocalizationViewModel>().ToArray();
}
return viewModels;
}
}
static bool IsMatch(CultureInfo cultureInfo, string cultureName)
{
if (string.IsNullOrWhiteSpace(cultureInfo.Name))
{
return false;
}
if (cultureInfo.Name == cultureName)
{
return true;
}
else
{
return IsMatch(cultureInfo.Parent, cultureName);
}
}
public void ChangeLanguage(string cultureName)
{
if (IsMatch(CultureInfo.CurrentUICulture, cultureName)) return;
if (!ViewModels.Any()) return;
void ChangeLanguage()
{
CultureInfo.CurrentUICulture = new CultureInfo(cultureName);
foreach (var viewModel in ViewModels)
{
viewModel.OnChangeLanguage();
}
}
MainThreadDesktop.BeginInvokeOnMainThread(ChangeLanguage);
}
}
}
@@ -0,0 +1,32 @@
using Microsoft.Extensions.DependencyInjection;
using System.Application.UI.ViewModels;
using System.Collections.Generic;
using System.Linq;
namespace System.Application.Services.Implementation
{
internal sealed class ViewModelCollectionServiceImpl : IViewModelCollectionService
{
readonly IEnumerable<ServiceDescriptor> services;
public ViewModelCollectionServiceImpl(IEnumerable<ServiceDescriptor> services)
{
this.services = services;
}
public IEnumerable<ViewModelBase> ViewModels => GetViewModels<ViewModelBase>();
public IEnumerable<T> GetViewModels<T>()
{
var tType = typeof(T);
var vType = typeof(ViewModelBase);
var tIsvType = tType == vType;
return from s in services
where s.ServiceType == s.ImplementationType &&
s.Lifetime == ServiceLifetime.Singleton &&
tType.IsAssignableFrom(s.ImplementationType) &&
(tIsvType || vType.IsAssignableFrom(s.ImplementationType))
select (T)DI.Get(s.ServiceType);
}
}
}
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>System.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="13.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools.Client\System.Application.SteamTools.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="UI\Resx\AppResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>AppResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="UI\Resx\AppResources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
@@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace System.Application.UI.Resx {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class AppResources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal AppResources() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Application.UI.Resx.AppResources", typeof(AppResources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 查找类似 关于 的本地化字符串。
/// </summary>
public static string About {
get {
return ResourceManager.GetString("About", resourceCulture);
}
}
/// <summary>
/// 查找类似 社区加速 的本地化字符串。
/// </summary>
public static string CommunityFix {
get {
return ResourceManager.GetString("CommunityFix", resourceCulture);
}
}
/// <summary>
/// 查找类似 库存游戏 的本地化字符串。
/// </summary>
public static string GameList {
get {
return ResourceManager.GetString("GameList", resourceCulture);
}
}
/// <summary>
/// 查找类似 游戏功能 的本地化字符串。
/// </summary>
public static string GameRelated {
get {
return ResourceManager.GetString("GameRelated", resourceCulture);
}
}
/// <summary>
/// 查找类似 语言 的本地化字符串。
/// </summary>
public static string Language {
get {
return ResourceManager.GetString("Language", resourceCulture);
}
}
/// <summary>
/// 查找类似 设置 的本地化字符串。
/// </summary>
public static string Settings {
get {
return ResourceManager.GetString("Settings", resourceCulture);
}
}
/// <summary>
/// 查找类似 令牌设置 的本地化字符串。
/// </summary>
public static string Settings_Auth {
get {
return ResourceManager.GetString("Settings_Auth", resourceCulture);
}
}
/// <summary>
/// 查找类似 基本设置 的本地化字符串。
/// </summary>
public static string Settings_General {
get {
return ResourceManager.GetString("Settings_General", resourceCulture);
}
}
/// <summary>
/// 查找类似 本地令牌 的本地化字符串。
/// </summary>
public static string SteamAuth {
get {
return ResourceManager.GetString("SteamAuth", resourceCulture);
}
}
/// <summary>
/// 查找类似 账号切换 的本地化字符串。
/// </summary>
public static string UserFastChange {
get {
return ResourceManager.GetString("UserFastChange", resourceCulture);
}
}
/// <summary>
/// 查找类似 起始页 的本地化字符串。
/// </summary>
public static string Welcome {
get {
return ResourceManager.GetString("Welcome", resourceCulture);
}
}
}
}
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="About" xml:space="preserve">
<value>About</value>
</data>
<data name="CommunityFix" xml:space="preserve">
<value>Fix Steam 302 Error</value>
</data>
<data name="GameList" xml:space="preserve">
<value>My Games</value>
</data>
<data name="GameRelated" xml:space="preserve">
<value>Game Related</value>
</data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Settings_Auth" xml:space="preserve">
<value>Auth Settings</value>
</data>
<data name="Settings_General" xml:space="preserve">
<value>General Settings</value>
</data>
<data name="SteamAuth" xml:space="preserve">
<value>Steam Auth</value>
</data>
<data name="UserFastChange" xml:space="preserve">
<value>Account Change</value>
</data>
<data name="Welcome" xml:space="preserve">
<value>Start Page</value>
</data>
</root>
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="About" xml:space="preserve">
<value>关于</value>
</data>
<data name="CommunityFix" xml:space="preserve">
<value>社区加速</value>
</data>
<data name="GameList" xml:space="preserve">
<value>库存游戏</value>
</data>
<data name="GameRelated" xml:space="preserve">
<value>游戏功能</value>
</data>
<data name="Language" xml:space="preserve">
<value>语言</value>
</data>
<data name="Settings" xml:space="preserve">
<value>设置</value>
</data>
<data name="Settings_Auth" xml:space="preserve">
<value>令牌设置</value>
</data>
<data name="Settings_General" xml:space="preserve">
<value>基本设置</value>
</data>
<data name="SteamAuth" xml:space="preserve">
<value>本地令牌</value>
</data>
<data name="UserFastChange" xml:space="preserve">
<value>账号切换</value>
</data>
<data name="Welcome" xml:space="preserve">
<value>起始页</value>
</data>
</root>
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="About" xml:space="preserve">
<value>關於</value>
</data>
<data name="CommunityFix" xml:space="preserve">
<value>社區加速</value>
</data>
<data name="GameList" xml:space="preserve">
<value>遊戲列表</value>
</data>
<data name="GameRelated" xml:space="preserve">
<value>遊戲功能</value>
</data>
<data name="Language" xml:space="preserve">
<value>語言</value>
</data>
<data name="Settings" xml:space="preserve">
<value>設定</value>
</data>
<data name="Settings_Auth" xml:space="preserve">
<value>令牌設定</value>
</data>
<data name="Settings_General" xml:space="preserve">
<value>基本設定</value>
</data>
<data name="SteamAuth" xml:space="preserve">
<value>本地令牌</value>
</data>
<data name="UserFastChange" xml:space="preserve">
<value>帳號切換</value>
</data>
<data name="Welcome" xml:space="preserve">
<value>起始頁</value>
</data>
</root>
@@ -0,0 +1,27 @@
using System.Application.UI.Resx;
using System.Linq;
using System.Reflection;
namespace System.Application.UI.ViewModels
{
/// <summary>
/// 本地化、多语言视图模型
/// </summary>
public interface ILocalizationViewModel
{
public void OnChangeLanguage()
{
var properties = GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
var query = from p in properties
let attr = p.GetCustomAttribute<ResStringAttribute>()
where p.PropertyType == typeof(string) && attr != null
select (p, attr.ResId);
foreach (var (p, ResId) in query)
{
var resId = ResId ?? p.Name;
var value = AppResources.ResourceManager.GetString(resId);
p.SetValue(this, value);
}
}
}
}
@@ -0,0 +1,13 @@
namespace System.Application.UI.ViewModels
{
[AttributeUsage(AttributeTargets.Property)]
public sealed class ResStringAttribute : Attribute
{
public string? ResId { get; }
public ResStringAttribute(string? resId = null)
{
ResId = resId;
}
}
}
@@ -0,0 +1,24 @@
using ReactiveUI;
namespace System.Application.UI.ViewModels
{
public class ViewModelBase : ReactiveObject, ILocalizationViewModel
{
public ViewModelBase()
{
ILocalizationViewModel localization = this;
localization.OnChangeLanguage();
}
/// <summary>
/// 是否在设计器的上下文中运行
/// </summary>
public static bool IsInDesignMode { get; set; }
protected static TViewModel GetViewModel<TViewModel>() where TViewModel : ViewModelBase, new()
{
if (IsInDesignMode) return new TViewModel();
return DI.Get<TViewModel>();
}
}
}
@@ -0,0 +1,15 @@
using ReactiveUI;
namespace System.Application.UI.ViewModels
{
public class WindowViewModel : ViewModelBase
{
string title = string.Empty;
public string Title
{
get => title;
set => this.RaiseAndSetIfChanged(ref title, value);
}
}
}
@@ -0,0 +1,56 @@
using System.IO;
namespace System.Application
{
/// <summary>
/// 指定用于检索应用程序所使用的文件夹目录路径的枚举常数
/// </summary>
public enum AppFolder
{
/// <summary>
/// 图片
/// </summary>
Images,
/// <summary>
/// 数据库
/// </summary>
Database,
/// <summary>
/// 缓存、临时文件夹(注意清理!)
/// </summary>
Cache,
}
/// <summary>
/// Enum 扩展 <see cref="AppFolder"/>
/// </summary>
public static class AppFolderEnumExtensions
{
/// <summary>
/// 获取文件夹路径,返回的路径必定存在
/// </summary>
/// <param name="folder"></param>
/// <returns></returns>
public static string GetPath(this AppFolder folder)
{
var path1 = folder switch
{
AppFolder.Images or AppFolder.Cache => IOPath.CacheDirectory,
AppFolder.Database => IOPath.AppDataDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(folder), folder, null),
};
var path = Path.Combine(path1, folder.ToString());
IOPath.DirCreateByNotExists(path);
return path;
}
#if DEBUG
[Obsolete("use GetPath", true)]
public static string GetFolderPath(this AppFolder folder) => folder.GetPath();
#endif
}
}
@@ -0,0 +1,22 @@
using SQLite;
using System.Diagnostics;
namespace System.Application.Entities
{
[Table("D5428AED")]
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class User : IEntity<Guid>
{
string DebuggerDisplay => Id.ToString();
[Column("5E72F0AE")]
[PrimaryKey]
public Guid Id { get; set; }
[Column("A931B798")]
public byte[]? NickName { get; set; }
[Column("4C12F9EE")]
public Guid? Avatar { get; set; }
}
}
@@ -0,0 +1,28 @@
using System.Application.Services.CloudService;
namespace System.Application
{
public static class ImageUrlHelper
{
static string ApiBaseUrl
{
get
{
var client = DI.Get<CloudServiceClientBase>();
return client.ApiBaseUrl;
}
}
/// <summary>
/// 获取图片的APIURL(相对路径)
/// </summary>
const string GetImageApiUrl = "{1}/api/images/{0}";
/// <summary>
/// 根据图片Id获取APIURL(绝对路径)
/// </summary>
/// <param name="imageId"></param>
/// <returns></returns>
public static string GetImageApiUrlById(Guid imageId) => string.Format(GetImageApiUrl, imageId, ApiBaseUrl);
}
}
@@ -0,0 +1,10 @@
namespace System.Application
{
public static class KeyConstants
{
/// <summary>
/// 当前登录用户
/// </summary>
public const string KEY_CURRENT_LOGIN_USER = "KEY_CURRENT_LOGIN_USER";
}
}
@@ -0,0 +1,53 @@
using System.Security.Cryptography;
using MPIgnore = MessagePack.IgnoreMemberAttribute;
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
[MPObj]
public sealed class AppSettings : IAppSettings, ICloudServiceSettings
{
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public Guid AppVersion { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public string? ApiBaseUrl { get; set; }
[MPKey(2)]
[N_JsonProperty("2")]
[S_JsonProperty("2")]
public byte[]? AesSecret { get; set; }
Aes? aes;
[MPIgnore]
[N_JsonIgnore]
[S_JsonIgnore]
public Aes Aes
{
get
{
if (aes == null)
{
if (AesSecret == null) throw new ArgumentNullException(nameof(AesSecret));
aes = AESUtils.Create(AesSecret);
}
return aes;
}
}
[MPKey(3)]
[N_JsonProperty("3")]
[S_JsonProperty("3")]
public string? AppSecretVisualStudioAppCenter { get; set; }
}
}
@@ -0,0 +1,44 @@
using System.Application.Columns;
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
/// <summary>
/// 当前登录用户模型,如需增加字段,还需要在 <see cref="Clone"/> 中赋值新添加字段
/// </summary>
[MPObj]
public sealed class CurrentUser : ILoginResponse, IExplicitHasValue, IPhoneNumber
{
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public Guid UserId { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public string? AuthToken { get; set; }
[MPKey(2)]
[N_JsonProperty("2")]
[S_JsonProperty("2")]
public string? PhoneNumber { get; set; }
bool IExplicitHasValue.ExplicitHasValue()
{
if (string.IsNullOrWhiteSpace(AuthToken)) return false;
return true;
}
public CurrentUser? Clone() => this.HasValue() ?
new CurrentUser
{
UserId = UserId,
AuthToken = AuthToken,
PhoneNumber = PhoneNumber,
} : null;
}
}
@@ -0,0 +1,33 @@
namespace System.Application
{
/// <summary>
/// 通知渠道类型(一个渠道可包含一个或多个<see cref="NotificationType"/>)
/// <list type="bullet">
/// <item>
/// <term>iOS:</term>
/// </item>
/// <item>
/// <term>Android:将枚举通过 GetChannelId 转换为 <see cref="string"/> 用作 NotificationCompat.Builder 中的 channelId</term>
/// </item>
/// <item>
/// <term>UWP:</term>
/// </item>
/// <item>
/// <term>Win32:</term>
/// </item>
/// </list>
/// <para>添加新的枚举常量注意事项:</para>
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetName(NotificationChannelType)"/>中添加渠道名称</para>
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetDescription(NotificationChannelType)"/>中添加渠道描述</para>
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetImportanceLevel(NotificationChannelType)"/>中添加渠道重要性级别</para>
/// </summary>
public enum NotificationChannelType
{
/// <summary>
/// 新版本
/// <para>例如:</para>
/// <para>新版本下载进度</para>
/// </summary>
NewVersion = 2,
}
}
@@ -0,0 +1,29 @@
namespace System.Application
{
/// <summary>
/// 通知类型
/// <list type="bullet">
/// <item>
/// <term>iOS:</term>
/// </item>
/// <item>
/// <term>Android:将枚举转换为 <see cref="int"/> 用作 Android.App.NotificationManager 中的 id</term>
/// </item>
/// <item>
/// <term>UWP:</term>
/// </item>
/// <item>
/// <term>Win32:</term>
/// </item>
/// </list>
/// <para>添加新的枚举常量注意事项:</para>
/// <para>(客户端)如果要显示在设备的通知栏上,则需要在<see cref="NotificationType_Channel_EnumExtensions.GetChannel(NotificationType)"/>中添加对应渠道</para>
/// </summary>
public enum NotificationType
{
/// <summary>
/// 新版本下载进度
/// </summary>
NewVersionDownloadProgress = 7,
}
}
@@ -0,0 +1,69 @@
using System.Application.Properties;
using System.Application.Services;
namespace System.Application
{
/// <summary>
/// Enum 扩展 <see cref="NotificationType"/> AND <see cref="NotificationChannelType"/>
/// </summary>
public static class NotificationType_Channel_EnumExtensions
{
/// <summary>
/// 获取所属的通知渠道
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static NotificationChannelType GetChannel(this NotificationType value) => value switch
{
NotificationType.NewVersionDownloadProgress => NotificationChannelType.NewVersion,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
/// <summary>
/// 获取渠道的用户可见名称
/// <para>建议的最大长度为40个字符,如果该值太长,可能会被截断</para>
/// <para>参考:https://developer.android.google.cn/reference/android/app/NotificationChannel?hl=en#setName%28java.lang.CharSequence%29 </para>
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static string GetName(this NotificationChannelType value)
{
return value switch
{
NotificationChannelType.NewVersion => SR.NotificationChannelType_Name_NewVersion,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
/// <summary>
/// 获取渠道的用户可见描述
/// <para>建议的最大长度为300个字符,如果该值太长,可能会被截断</para>
/// <para>参考:https://developer.android.google.cn/reference/android/app/NotificationChannel?hl=en#setDescription%28java.lang.String%29 </para>
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static string GetDescription(this NotificationChannelType value)
{
return value switch
{
NotificationChannelType.NewVersion => SR.NotificationChannelType_Description_NewVersion,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
/// <summary>
/// 获取渠道的重要性级别
/// <para>参考:https://developer.android.google.cn/reference/android/app/NotificationChannel?hl=en#getImportance%28%29 </para>
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static NotificationImportanceLevel GetImportanceLevel(this NotificationChannelType value)
=> value switch
{
NotificationChannelType.NewVersion => NotificationImportanceLevel.Medium,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
}
@@ -0,0 +1,108 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace System.Application.Properties {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Application.Properties.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 查找类似 Steam++ 的本地化字符串。
/// </summary>
internal static string AppName {
get {
return ResourceManager.GetString("AppName", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在下载:{0}% 的本地化字符串。
/// </summary>
internal static string Downloading_ {
get {
return ResourceManager.GetString("Downloading_", resourceCulture);
}
}
/// <summary>
/// 查找类似 新版本更新 的本地化字符串。
/// </summary>
internal static string NewVersionUpdate {
get {
return ResourceManager.GetString("NewVersionUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 当下载新版本时的下载进度条通知。 的本地化字符串。
/// </summary>
internal static string NotificationChannelType_Description_NewVersion {
get {
return ResourceManager.GetString("NotificationChannelType_Description_NewVersion", resourceCulture);
}
}
/// <summary>
/// 查找类似 新版本通知 的本地化字符串。
/// </summary>
internal static string NotificationChannelType_Name_NewVersion {
get {
return ResourceManager.GetString("NotificationChannelType_Name_NewVersion", resourceCulture);
}
}
}
}
@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppName" xml:space="preserve">
<value>Steam++</value>
</data>
<data name="Downloading_" xml:space="preserve">
<value>正在下载:{0}%</value>
</data>
<data name="NewVersionUpdate" xml:space="preserve">
<value>新版本更新</value>
</data>
<data name="NotificationChannelType_Description_NewVersion" xml:space="preserve">
<value>当下载新版本时的下载进度条通知。</value>
</data>
<data name="NotificationChannelType_Name_NewVersion" xml:space="preserve">
<value>新版本通知</value>
</data>
</root>
@@ -0,0 +1,8 @@
using System.Application.Entities;
namespace System.Application.Repositories
{
public interface IUserRepository : IRepository<User, Guid>
{
}
}
@@ -0,0 +1,8 @@
using System.Application.Entities;
namespace System.Application.Repositories.Implementation
{
internal sealed class UserRepository : Repository<User, Guid>, IUserRepository
{
}
}
@@ -0,0 +1,20 @@
using System.Application.Properties;
namespace System.Application.Services
{
/// <inheritdoc cref="INotificationService{TNotificationType}"/>
public interface INotificationService : INotificationService<NotificationType>
{
/// <summary>
/// 下载新版本进度通知
/// </summary>
/// <returns></returns>
public Progress<float> NotifyNewVersionDownloadProgress()
{
return NotifyDownload(
SR.Downloading_,
NotificationType.NewVersionDownloadProgress,
SR.AppName + SR.NewVersionUpdate);
}
}
}
@@ -0,0 +1,28 @@
namespace System.Application.Services
{
/// <summary>
/// 安全服务
/// </summary>
public interface ISecurityService
{
/// <summary>
/// 加密
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
byte[]? E(string? value);
/// <inheritdoc cref="E(string?)"/>
byte[]? EB(byte[]? value);
/// <summary>
/// 解密
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
string? D(byte[]? value);
/// <inheritdoc cref="D(byte[]?)"/>
byte[]? DB(byte[]? value);
}
}
@@ -0,0 +1,64 @@
using System.Application.Models;
using System.Threading.Tasks;
namespace System.Application.Services
{
/// <summary>
/// 用户管理
/// </summary>
public interface IUserManager : IAuthHelper
{
/// <inheritdoc cref="GetCurrentUserAsync"/>
CurrentUser? GetCurrentUser();
/// <summary>
/// 获取当前登录用户
/// <para>如果[退出登录]则为 <see langword="null"/>,对于接收到的推送消息,要求在服务端时传入接收人用户Id,客户端根据Id读取用户信息,而不使用此值</para>
/// </summary>
/// <returns></returns>
ValueTask<CurrentUser?> GetCurrentUserAsync();
/// <summary>
/// 设置当前登录用户,当[退出登录]时可传入<see langword="null"/>
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
Task SetCurrentUserAsync(CurrentUser? value);
///// <summary>
///// 获取当前登录用户资料
///// <para>如果[退出登录]则为 <see langword="null"/>,对于接收到的推送消息,要求在服务端时传入接收人用户Id,客户端根据Id读取用户信息,而不使用此值</para>
///// </summary>
///// <returns></returns>
//ValueTask<UserInfoDTO?> GetCurrentUserInfoAsync();
///// <summary>
///// 设置当前登录用户资料
///// </summary>
///// <param name="value"></param>
///// <param name="updateToDataBase">更新到数据库中</param>
///// <returns></returns>
//Task SetCurrentUserInfoAsync(UserInfoDTO value, bool updateToDataBase);
/// <summary>
/// 根据Id获取用户数据
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<UserDTO?> GetUserByIdAsync(Guid userId);
///// <summary>
///// 根据Id获取用户资料
///// </summary>
///// <param name="userId"></param>
///// <returns></returns>
//Task<UserInfoDTO?> GetUserInfoByIdAsync(Guid userId);
/// <summary>
/// 添加或更新用户数据到数据库中
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task InsertOrUpdateAsync(IUserDTO user);
}
}
@@ -0,0 +1,54 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Application.Columns;
using System.Application.Models;
using System.Application.Services.CloudService;
using System.Threading.Tasks;
namespace System.Application.Services.Implementation
{
public abstract class CloudServiceClient : CloudServiceClientBase
{
public const string TAG = "ServerApiClient";
protected readonly IUserManager userManager;
protected readonly IToast toast;
public CloudServiceClient(
ILoggerFactory loggerFactory,
IUserManager userManager,
IToast toast,
IOptions<AppSettings> options,
IModelValidator validator) : base(loggerFactory.CreateLogger(TAG), userManager, options, validator)
{
this.userManager = userManager;
this.toast = toast;
}
public override async Task SaveAuthTokenAsync(string authToken)
{
var user = await userManager.GetCurrentUserAsync();
if (user != null)
{
user.AuthToken = authToken;
await userManager.SetCurrentUserAsync(user);
}
}
public override async Task OnLoginedAsync(IReadOnlyPhoneNumber? phoneNumber, ILoginResponse response)
{
var cUser = new CurrentUser
{
UserId = response.UserId,
AuthToken = response.AuthToken,
PhoneNumber = phoneNumber?.PhoneNumber ?? string.Empty,
};
await userManager.SetCurrentUserAsync(cUser);
}
public override void ShowResponseErrorMessage(string message)
{
toast.Show(message);
}
}
}
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Options;
using System.Application.Models;
using System.Security.Cryptography;
namespace System.Application.Services.Implementation
{
/// <inheritdoc cref="ISecurityService"/>
public class SecurityService : ISecurityService
{
protected readonly AppSettings settings;
public SecurityService(IOptions<AppSettings> options)
{
settings = options.Value;
}
public virtual byte[]? E(string? value) => AESUtils.EncryptToByteArray_Nullable(settings.Aes, value);
public virtual byte[]? EB(byte[]? value) => AESUtils.Encrypt_Nullable(settings.Aes, value);
public virtual string? D(byte[]? value) => AESUtils.DecryptToString_Nullable(settings.Aes, value);
public virtual byte[]? DB(byte[]? value) => AESUtils.Decrypt_Nullable(settings.Aes, value);
}
}
@@ -0,0 +1,260 @@
using Microsoft.Extensions.Logging;
using System.Application.Entities;
using System.Application.Models;
using System.Application.Repositories;
using System.Diagnostics;
using System.Security;
using System.Threading.Tasks;
using static System.Application.KeyConstants;
namespace System.Application.Services.Implementation
{
/// <inheritdoc cref="IUserManager"/>
public class UserManager : IUserManager
{
protected const string TAG = "UserManager";
protected readonly IStorage storage;
protected readonly ILogger logger;
protected readonly IUserRepository userRepository;
protected readonly ISecurityService security;
public UserManager(
ILoggerFactory loggerFactory,
IStorage storage,
IUserRepository userRepository,
ISecurityService security)
{
logger = loggerFactory.CreateLogger(TAG);
this.storage = storage;
this.userRepository = userRepository;
this.security = security;
}
protected bool isAnonymous;
protected CurrentUser? currentUser;
//protected UserInfoDTO? currentUserInfo;
protected CurrentUser? CurrentUser
{
set
{
currentUser = value;
isAnonymous = value == null;
}
}
[Conditional("DEBUG")]
void PrintCurrentUser(string name)
{
logger.LogInformation(
$"{name}: {currentUser?.ToStringHideMiddleFour()}");
}
protected async ValueTask<CurrentUser?> GetCurrentUserAsync(bool clone)
{
if (currentUser == null && !isAnonymous)
{
try
{
CurrentUser = await storage.GetAsync<CurrentUser>(KEY_CURRENT_LOGIN_USER);
}
catch (Exception e)
{
logger.LogError(e, nameof(GetCurrentUserAsync));
}
PrintCurrentUser(nameof(GetCurrentUserAsync));
}
return clone ? currentUser?.Clone() : currentUser;
}
public CurrentUser? GetCurrentUser()
{
var hasCurrentUser = currentUser != null;
#if DEBUG
var read_cache = Random2.Next(100) % 2 == 0;
hasCurrentUser = read_cache && hasCurrentUser;
#endif
CurrentUser? result;
if (hasCurrentUser)
{
result = currentUser?.Clone();
}
else
{
Func<ValueTask<CurrentUser?>> func = GetCurrentUserAsync;
result = func.RunSync();
}
#if DEBUG
logger.LogDebug("read_cache: {0}, PhoneNumber: {1}", read_cache, result?.ToStringHideMiddleFour());
#endif
return result;
}
public ValueTask<CurrentUser?> GetCurrentUserAsync()
{
return GetCurrentUserAsync(true);
}
public async Task SetCurrentUserAsync(CurrentUser? value)
{
await storage.SetAsync(KEY_CURRENT_LOGIN_USER, currentUser);
CurrentUser = value;
PrintCurrentUser("SetCurrentUser");
}
//public async ValueTask<UserInfoDTO?> GetCurrentUserInfoAsync()
//{
// if (currentUserInfo == null && !isAnonymous)
// {
// var cUser = await GetCurrentUserAsync();
// if (cUser != null)
// {
// currentUserInfo = await GetUserInfoByIdAsync(cUser.UserId);
// }
// }
// return currentUserInfo;
//}
//public async Task SetCurrentUserInfoAsync(UserInfoDTO value, bool updateToDataBase)
//{
// currentUserInfo = value;
// if (updateToDataBase)
// {
// await InsertOrUpdateAsync(value);
// }
//}
public async ValueTask<string?> GetAuthTokenAsync()
{
var value = await GetCurrentUserAsync(false);
return value?.AuthToken;
}
public async Task SignOutAsync()
{
PrintCurrentUser("SignOut");
//currentUserInfo = default;
await SetCurrentUserAsync(null);
}
ValueTask<User?> GetUserTableByIdAsync(Guid userId)
{
return userRepository.FindAsync(userId);
}
TUserDTO? GetUserByTable<TUserDTO>(User? user, Func<User, TUserDTO?> binding) where TUserDTO : IUserDTO
{
if (user == null) return default;
var value = binding.Invoke(user);
return value;
}
TUserDTO? BindingUser<TUserDTO>(User user) where TUserDTO : IUserDTO, new()
{
var value = new TUserDTO
{
Id = user.Id,
NickName = security.D(user.NickName),
Avatar = user.Avatar,
};
return value;
}
public async Task<UserDTO?> GetUserByIdAsync(Guid userId)
{
var user = await GetUserTableByIdAsync(userId);
var value = GetUserByTable(user, BindingUser<UserDTO>);
return value;
}
//bool VerifyUserInfo(User user, UserInfoDTO userInfo)
//{
// if (user.Id != userInfo.Id)
// {
// logger.LogError("VerifyUserInfo Fail(Id).");
// return false;
// }
// if (security.D(user.NickName) != userInfo.NickName)
// {
// logger.LogError("VerifyUserInfo Fail(NickName).");
// return false;
// }
// if (user.Avatar != userInfo.Avatar)
// {
// logger.LogError("VerifyUserInfo Fail(Avatar).");
// return false;
// }
// return true;
//}
//UserInfoDTO? BindingUserInfo(User user)
//{
// if (user.UserInfo != null)
// {
// try
// {
// var userInfoBytes = security.DB(user.UserInfo);
// if (userInfoBytes != null)
// {
// var value = Serializable.DMP<UserInfoDTO>(userInfoBytes);
// if (value != null)
// {
// var v = VerifyUserInfo(user, value);
// if (v)
// {
// return value;
// }
// }
// }
// }
// catch (Exception e)
// {
// logger.LogError(e, "BindingUserInfo Serializable & Verify Fail.");
// }
// }
// return BindingUser<UserInfoDTO>(user);
//}
//public async Task<UserInfoDTO?> GetUserInfoByIdAsync(Guid userId)
//{
// var user = await GetUserTableByIdAsync(userId);
// return GetUserByTable(user, BindingUserInfo);
//}
public async Task InsertOrUpdateAsync(IUserDTO user)
{
var userTable = new User
{
Id = user.Id,
NickName = security.E(user.NickName),
Avatar = user.Avatar,
};
//if (user is UserInfoDTO userInfo)
//{
// userTable.UserInfo = Serializable.SMP(userInfo);
// userTable.UserInfo = security.EB(userTable.UserInfo);
//}
//else
//{
// var userTable2 = await GetUserTableByIdAsync(user.Id);
// if (userTable2?.UserInfo != null)
// {
// userTable.UserInfo = userTable2.UserInfo;
// }
//}
(var rowCount, var result) = await userRepository.InsertOrUpdateAsync(userTable);
PrintInsertOrUpdateResult(user, rowCount, result);
}
[Conditional("DEBUG")]
void PrintInsertOrUpdateResult(IUserDTO user, int rowCount, DbRowExecResult result)
{
logger.LogInformation(
$"User.InsertOrUpdate " +
$"rowCount: {rowCount}, " +
$"result: {result}, " +
$"user: [{user.NickName}-{user.Id}]");
}
}
}
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>System.Application</RootNamespace>
<DefineConstants>ORM_SQLITE_PCL;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools.Services.CloudService\System.Application.SteamTools.Services.CloudService.csproj" />
<ProjectReference Include="..\System.Application.SteamTools\System.Application.SteamTools.csproj" />
<ProjectReference Include="..\System.Common.ClientLib\System.Common.ClientLib.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
@@ -0,0 +1,100 @@
using MessagePack;
using Newtonsoft.Json;
using System.Application.Properties;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace System.Application.Models
{
public static class ApiResponse
{
static IApiResponse<T> ClientDeserializeFail<T>() => Code<T>(ApiResponseCode.ClientDeserializeFail);
static Type GetDeserializeType<T>()
{
if (typeof(T) == typeof(object)) return typeof(ApiResponseImpl);
return typeof(ApiResponseImpl<T>);
}
public static IApiResponse<T> Deserialize<T>(
JsonSerializer jsonSerializer,
JsonReader jsonReader)
{
var type = GetDeserializeType<T>();
var obj = (IApiResponse<T>?)jsonSerializer.Deserialize(jsonReader, type);
return obj ?? ClientDeserializeFail<T>();
}
public static async ValueTask<IApiResponse<T>> DeserializeAsync<T>(
Stream stream,
CancellationToken cancellationToken)
{
var type = GetDeserializeType<T>();
var obj = await MessagePackSerializer.DeserializeAsync(
type, stream, cancellationToken: cancellationToken);
return (IApiResponse<T>?)obj ?? ClientDeserializeFail<T>();
}
public static bool TryGetContent<T>(
this IApiResponse<T> response,
[NotNullWhen(true)] out T? content)
{
content = response.Content;
return response.IsSuccess && content != null;
}
public static string GetMessage(ApiResponseCode code)
{
if (code == ApiResponseCode.Unauthorized)
{
return SR.ApiResponseCode_Unauthorized;
}
return string.Format(SR.ServerError_, (int)code);
}
public static string GetMessage(IApiResponse response)
{
#pragma warning disable CS0618 // 类型或成员已过时
var message = response.Message;
#pragma warning restore CS0618 // 类型或成员已过时
if (string.IsNullOrWhiteSpace(message))
{
return GetMessage(response.Code);
}
else
{
return message;
}
}
public static IApiResponse Code(ApiResponseCode code, string? message = null) => new ApiResponseImpl
{
Code = code,
Message = message,
};
static readonly Lazy<IApiResponse> okRsp = new Lazy<IApiResponse>(() => Code(ApiResponseCode.OK));
public static IApiResponse Ok() => okRsp.Value;
public static IApiResponse<T> Code<T>(
ApiResponseCode code,
string? message = null,
T? content = default) => new ApiResponseImpl<T>
{
Code = code,
Message = message,
Content = content,
};
public static IApiResponse<T> Ok<T>(T? content = default)
=> Code(ApiResponseCode.OK, content: content);
public static IApiResponse Fail() => Code(ApiResponseCode.Fail);
public static IApiResponse<T> Fail<T>(string? message = null)
=> Code<T>(ApiResponseCode.Fail, message);
}
}
@@ -0,0 +1,106 @@
using System.Net;
namespace System.Application.Models
{
/// <summary>
/// MAS 定义响应码
/// <para>200~299代表成功,三位数对应HTTP状态码,四位数为本业务自定义响应码</para>
/// </summary>
public enum ApiResponseCode
{
#region HttpStatusCode 100 ~ 511
/// <summary>
/// 成功
/// </summary>
OK = HttpStatusCode.OK,
Unauthorized = HttpStatusCode.Unauthorized,
#endregion
#region CustomCode 自定义响应码,1000~?
/// <summary>
/// 无响应正文内容
/// </summary>
NoResponseContent = 1000,
/// <summary>
/// 无响应正文内容值, <see cref="ExplicitHasValueExtensions.HasValue(IExplicitHasValue?)"/> 验证失败
/// </summary>
NoResponseContentValue,
/// <summary>
/// 失败
/// </summary>
Fail,
/// <summary>
/// 客户端反序列化失败
/// </summary>
ClientDeserializeFail,
/// <summary>
/// 不支持的响应媒体类型
/// </summary>
UnsupportedResponseMediaType,
/// <summary>
/// 不支持的上传文件媒体类型
/// </summary>
UnsupportedUploadFileMediaType,
/// <summary>
/// 客户端抛出异常
/// </summary>
ClientException,
/// <summary>
/// 被取消
/// </summary>
Canceled,
/// <summary>
/// 找不到HTTP请求授权头
/// </summary>
MissingAuthorizationHeader,
/// <summary>
/// HTTP请求授权声明不正确
/// </summary>
AuthSchemeNotCorrect,
/// <summary>
/// 用户被封禁
/// </summary>
UserIsBan,
/// <summary>
/// 找不到用户
/// </summary>
UserNotFound,
/// <summary>
/// 文件上传失败,缺少需要上传的有效文件
/// </summary>
LackAvailableUploadFile,
/// <summary>
/// 文件上传失败,上传的文件数与服务端响应结果数不相等
/// </summary>
UnequalLengthUploadFile,
/// <summary>
/// 请求模型验证失败
/// </summary>
RequestModelValidateFail,
/// <summary>
/// 网络连接中断
/// </summary>
NetworkConnectionInterruption,
#endregion
}
}
@@ -0,0 +1,64 @@
using MPIgnore = MessagePack.IgnoreMemberAttribute;
using MPKey = MessagePack.KeyAttribute;
using MPObject = MessagePack.MessagePackObjectAttribute;
using NJSONIgnore = Newtonsoft.Json.JsonIgnoreAttribute;
using NJsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using SJSONIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute;
using SJsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
internal abstract class ApiResponseImplBase : Serializable, IApiResponse
{
ApiResponseCode mCode;
bool mIsSuccess;
[MPKey(0)]
[NJsonProperty("🦄")]
[SJsonProperty("🦄")]
public ApiResponseCode Code
{
get => mCode;
set
{
mCode = value;
// https://github.com/dotnet/corefx/blob/v3.1.6/src/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L143
var code = (int)mCode;
mIsSuccess = code >= 200 && code <= 299;
}
}
[MPKey(LastMKeyIndex)]
[NJsonProperty("🐴")]
[SJsonProperty("🐴")]
public string? Message { get; set; }
/// <summary>
/// 最后一个 MessagePack 序列化 下标,继承自此类,新增需要序列化的字段/属性,标记此值+1,+2
/// </summary>
protected const int LastMKeyIndex = 1;
[MPIgnore]
[SJSONIgnore]
[NJSONIgnore]
public bool IsSuccess => mIsSuccess;
}
[MPObject]
internal class ApiResponseImpl<T> : ApiResponseImplBase, IApiResponse<T>
{
[MPKey(LastMKeyIndex + 1)]
[NJsonProperty("🦓")]
[SJsonProperty("🦓")]
public T? Content { get; set; }
}
[MPObject]
internal class ApiResponseImpl : ApiResponseImplBase, IApiResponse<object>
{
[MPIgnore]
[SJSONIgnore]
[NJSONIgnore]
public object? Content => null;
}
}
@@ -0,0 +1,37 @@
using System.Diagnostics.CodeAnalysis;
namespace System.Application.Models
{
/// <summary>
/// API响应模型
/// </summary>
public interface IApiResponse
{
/// <summary>
/// 响应码
/// </summary>
ApiResponseCode Code { get; set; }
/// <summary>
/// 显示消息
/// </summary>
[Obsolete("use IsSuccess()")]
string? Message { get; set; }
/// <summary>
/// 是否成功
/// </summary>
/// <returns></returns>
bool IsSuccess { get; }
}
/// <inheritdoc cref="IApiResponse"/>
public interface IApiResponse<out T> : IApiResponse
{
/// <summary>
/// 附加内容
/// </summary>
[MaybeNull]
T Content { get; }
}
}
@@ -0,0 +1,13 @@
namespace System.Application.Models
{
/// <summary>
/// 登录响应内容
/// </summary>
public interface ILoginResponse : IReadOnlyAuthToken
{
/// <summary>
/// 用户Id
/// </summary>
Guid UserId { get; }
}
}
@@ -0,0 +1,11 @@
namespace System.Application.Models
{
/// <summary>
/// 获取登录凭证
/// </summary>
public interface IReadOnlyAuthToken
{
/// <inheritdoc cref="IReadOnlyAuthToken"/>
string? AuthToken { get; }
}
}
@@ -0,0 +1,19 @@
namespace System.Application.Models
{
public interface IUserDTO
{
Guid Id { get; set; }
/// <summary>
/// 昵称
/// </summary>
string? NickName { get; set; }
/// <summary>
/// 头像
/// </summary>
Guid? Avatar { get; set; }
protected static string GetDebuggerDisplay(IUserDTO user) => $"{user.NickName}: {user.Id}";
}
}
@@ -0,0 +1,25 @@
using System.Application.Columns;
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
/// <summary>
/// 登录或注册接口请求模型
/// </summary>
[MPObj]
public class LoginOrRegisterRequest : IReadOnlyPhoneNumber, IReadOnlySmsCode
{
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public string? PhoneNumber { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public string? SmsCode { get; set; }
}
}
@@ -0,0 +1,51 @@
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
/// <summary>
/// 登录或注册接口响应模型
/// </summary>
[MPObj]
public class LoginOrRegisterResponse : ILoginResponse, IExplicitHasValue
{
Guid ILoginResponse.UserId => User?.Id ?? throw new ArgumentNullException(nameof(User));
public Guid UserId { get; set; }
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public string? AuthToken { get; set; }
/// <summary>
/// 当前登录的用户信息
/// </summary>
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public UserDTO? User { get; set; }
/// <summary>
/// 当前操作是登录(<see langword="true"/>)还是注册(<see langword="false"/>)
/// </summary>
[MPKey(2)]
[N_JsonProperty("2")]
[S_JsonProperty("2")]
public bool IsLoginOrRegister { get; set; }
bool IExplicitHasValue.ExplicitHasValue()
{
if (IsLoginOrRegister)
{
return true;
}
else
{
return User != null;
}
}
}
}
@@ -0,0 +1,22 @@
using System.Application.Columns;
using MPKey = MessagePack.KeyAttribute;
using MPObj = MessagePack.MessagePackObjectAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
[MPObj]
public class SendSmsRequest : IReadOnlyPhoneNumber
{
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public string? PhoneNumber { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public SmsCodeType Type { get; set; }
}
}
@@ -0,0 +1,30 @@
using System.Diagnostics;
using MPKey = MessagePack.KeyAttribute;
using MPObject = MessagePack.MessagePackObjectAttribute;
using N_JsonProperty = Newtonsoft.Json.JsonPropertyAttribute;
using S_JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
namespace System.Application.Models
{
[MPObject]
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class UserDTO : IUserDTO
{
string DebuggerDisplay => IUserDTO.GetDebuggerDisplay(this);
[MPKey(0)]
[N_JsonProperty("0")]
[S_JsonProperty("0")]
public Guid Id { get; set; }
[MPKey(1)]
[N_JsonProperty("1")]
[S_JsonProperty("1")]
public string? NickName { get; set; }
[MPKey(2)]
[N_JsonProperty("2")]
[S_JsonProperty("2")]
public Guid? Avatar { get; set; }
}
}
@@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace System.Application.Properties {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Application.Properties.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 查找类似 登录凭证失效,请重新登录 的本地化字符串。
/// </summary>
internal static string ApiResponseCode_Unauthorized {
get {
return ResourceManager.GetString("ApiResponseCode_Unauthorized", resourceCulture);
}
}
/// <summary>
/// 查找类似 网络连接中断 的本地化字符串。
/// </summary>
internal static string NetworkConnectionInterruption {
get {
return ResourceManager.GetString("NetworkConnectionInterruption", resourceCulture);
}
}
/// <summary>
/// 查找类似 服务端错误 {0} 的本地化字符串。
/// </summary>
internal static string ServerError_ {
get {
return ResourceManager.GetString("ServerError_", resourceCulture);
}
}
}
}
@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ApiResponseCode_Unauthorized" xml:space="preserve">
<value>登录凭证失效,请重新登录</value>
</data>
<data name="NetworkConnectionInterruption" xml:space="preserve">
<value>网络连接中断</value>
</data>
<data name="ServerError_" xml:space="preserve">
<value>服务端错误 {0}</value>
</data>
</root>
@@ -0,0 +1,29 @@
using System.Application.Properties;
namespace System.Application.Services.CloudService
{
public static class Constants
{
public const string Basic = "Basic";
public const string DefaultUserAgent = "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263";
public static string NetworkConnectionInterruption => SR.NetworkConnectionInterruption;
public const string HeaderAppVersion = "App-Version";
/// <summary>
/// 短信间隔,60秒
/// </summary>
public const int SMSInterval = 60;
/// <summary>
/// 实际短信间隔
/// </summary>
public const double SMSIntervalActually = SMSInterval * .95;
public const string Prefix_HTTPS = "https://";
public const string Prefix_HTTP = "http://";
}
}
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>System.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools\System.Application.SteamTools.csproj" />
<ProjectReference Include="..\System.Common.CoreLib\System.Common.CoreLib.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
@@ -0,0 +1,9 @@
namespace System.Application.Models
{
public interface ICloudServiceSettings
{
string? ApiBaseUrl { get; set; }
Guid AppVersion { get; set; }
}
}
@@ -0,0 +1,63 @@
using System.IO;
namespace System.Application.Models
{
/// <summary>
/// 上传文件模型
/// </summary>
public interface IUploadFileSource : IExplicitHasValue
{
/// <summary>
/// 开启读取流
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Stream OpenRead() => File.OpenRead(FilePath);
/// <summary>
/// 所在本地文件路径
/// </summary>
string FilePath { get; }
/// <summary>
/// 媒体类型
/// </summary>
string MIME { get; set; }
/// <summary>
/// 是否为上传优化过的文件
/// </summary>
bool IsCompressed { get; }
/// <summary>
/// 是否为缓存文件
/// </summary>
bool IsCache { get; }
UploadFileType UploadFileType { get; }
/// <summary>
/// 释放资源
/// </summary>
public void Dispose() => DeleteByCache(this);
/// <summary>
/// 如果为缓存文件则删除文件
/// </summary>
/// <param name="uploadFile"></param>
public static void DeleteByCache(IUploadFileSource uploadFile)
{
if (uploadFile.IsCache &&
!string.IsNullOrWhiteSpace(uploadFile.FilePath) &&
File.Exists(uploadFile.FilePath))
{
File.Delete(uploadFile.FilePath);
}
}
/// <summary>
/// 当前模型是否可上传
/// </summary>
public bool Available => true;
}
}
@@ -0,0 +1,17 @@
namespace System.Application.Models
{
public class UploadFileSource : IUploadFileSource
{
public string FilePath { get; set; } = string.Empty;
public string MIME { get; set; } = string.Empty;
public bool IsCompressed { get; set; }
public bool IsCache { get; set; }
public UploadFileType UploadFileType { get; set; }
bool IExplicitHasValue.ExplicitHasValue() => !string.IsNullOrWhiteSpace(FilePath);
}
}
@@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Application.Services;
using System.Application.Services.CloudService;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// 尝试添加 CloudServiceClient
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection TryAddCloudServiceClient<T>(this IServiceCollection services)
where T : CloudServiceClientBase
{
services.TryAddSingleton<T>();
services.TryAddSingleton<ICloudServiceClient>(s => s.GetRequiredService<T>());
services.TryAddSingleton<CloudServiceClientBase>(s => s.GetRequiredService<T>());
return services;
}
}
}
@@ -0,0 +1,633 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Application.Columns;
using System.Application.Models;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.FileFormats;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Essentials;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace System.Application.Services.CloudService
{
internal sealed class ApiConnection : IApiConnection
{
readonly ILogger logger;
readonly HttpClient client;
readonly IApiConnectionPlatformHelper helper;
readonly Lazy<JsonSerializer> jsonSerializer = new Lazy<JsonSerializer>(() => new JsonSerializer());
readonly IModelValidator validator;
public ApiConnection(ILogger logger, HttpClient client, IApiConnectionPlatformHelper helper, IModelValidator validator)
{
this.logger = logger;
this.client = client;
this.helper = helper;
this.validator = validator;
}
async ValueTask SetRequestHeaderAuthorization(HttpRequestMessage request)
{
var authToken = await helper.Auth.GetAuthTokenAsync();
if (!string.IsNullOrEmpty(authToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Basic, authToken);
}
}
/// <summary>
/// 根据异常获取响应码
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
ApiResponseCode GetCodeByException(Exception ex)
{
logger.LogError(ex, nameof(GetCodeByException));
if (ex is ApiResponseCodeException apiResponseCodeException)
{
return apiResponseCodeException.Code;
}
var isCanceled = ex.IsCanceledException();
if (isCanceled)
{
return ApiResponseCode.Canceled;
}
return ApiResponseCode.ClientException;
}
/// <summary>
/// 返回 HTTP 401 未授权,清空当前 AuthToken,并调用 SignOut
/// </summary>
/// <param name="requestUri"></param>
async Task Unauthorized(HttpMethod method, string requestUri)
{
logger.LogInformation("Unauthorized method: {0}, requestUri: {1}", method, requestUri);
await helper.Auth.SignOutAsync();
}
/// <summary>
/// 生成请求内容
/// </summary>
/// <typeparam name="TRequestModel">请求模型类型</typeparam>
/// <param name="serializableImplType">序列化实现类型,如果为上传文件,则此参数无效</param>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="request">请求模型</param>
/// <returns></returns>
HttpContent? GetRequestContent<TRequestModel>(
Serializable.ImplType serializableImplType,
TRequestModel? request,
CancellationToken cancellationToken)
{
if (request == null) return null;
HttpContent? httpContent;
if (request is IUploadFileSource uploadFile) // 上传单个文件
{
httpContent = GetMultipartFormDataContent1(uploadFile);
}
else if (request is IEnumerable<IUploadFileSource> uploadFiles) // 上传多个文件
{
httpContent = GetMultipartFormDataContent2(uploadFiles);
}
else
{
switch (serializableImplType) // 序列化模型
{
case Serializable.ImplType.NewtonsoftJson:
httpContent = GetJsonContent(Serializable.SJSON(Serializable.JsonImplType.NewtonsoftJson, request));
break;
case Serializable.ImplType.MessagePack:
var byteArray = Serializable.SMP(request, cancellationToken);
if (byteArray == null) return null;
httpContent = new ByteArrayContent(byteArray);
httpContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.MessagePack);
break;
case Serializable.ImplType.SystemTextJson:
httpContent = GetJsonContent(Serializable.SJSON(Serializable.JsonImplType.SystemTextJson, request));
break;
default:
throw new ArgumentOutOfRangeException(nameof(serializableImplType), serializableImplType, null);
}
}
return httpContent;
static StringContent? GetJsonContent(string? jsonStr)
{
if (jsonStr == null) return null;
return new StringContent(jsonStr, Encoding.UTF8, MediaTypeNames.JSON);
}
MultipartFormDataContent GetMultipartFormDataContent1(params IUploadFileSource[] uploadFiles)
{
var uploadFiles_ = uploadFiles.AsEnumerable();
return GetMultipartFormDataContent2(uploadFiles_);
}
MultipartFormDataContent GetMultipartFormDataContent2(IEnumerable<IUploadFileSource> uploadFiles)
{
var multipartFormDataContent = new MultipartFormDataContent();
int index = 0;
foreach (var item in uploadFiles)
{
if (item.HasValue() && item.Available)
{
var uploadFile = item;
var stream = uploadFile.OpenRead();
void ThrowUnsupportedUploadFileMediaType() // 未知的上传媒体类型
{
stream?.Dispose();
multipartFormDataContent.Dispose();
// ↑ 释放未压缩的文件流 与 总内容(MultipartFormDataContent) 当前文件源不释放
var msg = $"Unsupported Upload File MediaType, filePath: {uploadFile.FilePath}, index: {index}";
logger.LogError(msg);
throw new ApiResponseCodeException(
ApiResponseCode.UnsupportedUploadFileMediaType, msg);
}
var needHandle = true;
switch (uploadFile.UploadFileType) // 上传文件类型
{
case UploadFileType.Image: // 图片
if (uploadFile.IsCompressed) // 文件源已压缩过
{
if (string.IsNullOrEmpty(uploadFile.MIME)) // 无MIME,则检测
{
if (FileFormat.IsImage(stream, out var format))
{
if (format.IsAllow()) // 属于允许的格式,则不处理
{
uploadFile.MIME = format.GetMIME();
needHandle = false;
}
}
}
else if (FileFormat.AllowImageMediaTypeNames.Contains(uploadFile.MIME))
{
// 有MIME值,且在允许的范围内,则不处理
needHandle = false;
}
}
break;
//case UploadFileType.Voice: // 音频
// break;
//case UploadFileType.Video: // 视频
// break;
default:
throw new ArgumentOutOfRangeException(nameof(uploadFile.UploadFileType), uploadFile.UploadFileType, null);
}
if (needHandle)
{
var result = helper.TryHandleUploadFile(stream, uploadFile.UploadFileType);
if (!result.HasValue) // 处理失败
{
ThrowUnsupportedUploadFileMediaType();
}
else
{
stream.Dispose();
uploadFile.Dispose();
// ↑ 释放未压缩的文件流 与 文件源
uploadFile = new UploadFileSource
{
FilePath = result.Value.filePath,
MIME = result.Value.mime,
IsCompressed = true,
IsCache = true,
UploadFileType = uploadFile.UploadFileType,
};
stream = uploadFile.OpenRead();
// 生成新的文件源 并 重新打开文件流
}
}
var content = new UploadFileContent(uploadFile, stream);
if (string.IsNullOrWhiteSpace(uploadFile.FilePath))
{
multipartFormDataContent.Add(content, "file");
}
else
{
var fileName = Path.GetFileName(uploadFile.FilePath);
multipartFormDataContent.Add(content, "file", fileName);
}
}
index++;
}
if (!multipartFormDataContent.Any())
{
throw new ApiResponseCodeException(ApiResponseCode.LackAvailableUploadFile);
}
return multipartFormDataContent;
}
}
void ShowResponseErrorMessage(IApiResponse response)
{
if (response.Code == ApiResponseCode.Canceled) return;
var message = ApiResponse.GetMessage(response);
helper.ShowResponseErrorMessage(message);
}
async Task GlobalResponseIntercept(
HttpMethod method,
string requestUri,
IApiResponse response)
{
if (!response.IsSuccess)
{
ShowResponseErrorMessage(response);
if (response.Code == ApiResponseCode.Unauthorized)
{
await Unauthorized(method, requestUri);
}
}
}
async Task GlobalResponseIntercept<TResponseModel>(
bool isApi,
HttpMethod method,
string requestUri,
object? request,
IApiResponse<TResponseModel> response,
bool responseContentMaybeNull)
{
if (response.IsSuccess)
{
if (!responseContentMaybeNull && response.Content == null)
{
response.Code = ApiResponseCode.NoResponseContent;
}
else
{
if (isApi)
{
if (response is IApiResponse<IExplicitHasValue> explicitHasValue && !explicitHasValue.Content.HasValue())
{
response.Code = ApiResponseCode.NoResponseContentValue;
}
else
{
if (response is IApiResponse<ILoginResponse> loginResponse
&& loginResponse.Content != null)
{
IReadOnlyPhoneNumber? phoneNumber;
if (loginResponse.Content is IReadOnlyPhoneNumber phoneNumber1)
phoneNumber = phoneNumber1;
else if (request is IReadOnlyPhoneNumber phoneNumber2)
phoneNumber = phoneNumber2;
else
phoneNumber = null;
await helper.OnLoginedAsync(phoneNumber, loginResponse.Content);
}
else if (response is IApiResponse<IReadOnlyAuthToken> authTokenResponse
&& authTokenResponse.Content != null)
{
var authToken = authTokenResponse.Content.AuthToken;
if (!string.IsNullOrEmpty(authToken))
{
await helper.SaveAuthTokenAsync(authToken);
}
}
else if (response is IApiResponse<Guid[]> guidsResponse
&& guidsResponse.Content != null)
{
if (request is IUploadFileSource uploadFile) // 上传单个文件
{
HandleUploadFile(uploadFile);
}
else if (request is IEnumerable<IUploadFileSource> uploadFiles) // 上传多个文件
{
HandleUploadFiles(uploadFiles);
}
void HandleUploadFile(params IUploadFileSource[] uploadFiles)
{
var uploadFiles_ = uploadFiles.AsEnumerable();
HandleUploadFiles(uploadFiles_);
}
void HandleUploadFiles(IEnumerable<IUploadFileSource> uploadFiles)
{
var items = uploadFiles.Where(x => x.HasValue() && x.Available).ToArray();
if (items.Length != guidsResponse.Content.Length)
{
var msg = $"Unequal Length Upload File " +
$"request: {items.Length}, response: {guidsResponse.Content.Length}";
logger.LogError(msg);
throw new ApiResponseCodeException(
ApiResponseCode.UnequalLengthUploadFile, msg);
}
else
{
// 上传后将此缓存文件移动到下载图片文件夹中
throw new NotImplementedException();
}
}
}
}
}
}
}
await GlobalResponseIntercept(method, requestUri, response);
}
/// <summary>
/// 是否有网络链接
/// </summary>
bool IsConnected
{
get
{
if (DI.Platform == Platform.Windows || DI.Platform == Platform.Linux || DI.Platform == Platform.Unknown) return true;
return Connectivity.NetworkAccess == NetworkAccess.Internet;
}
}
bool GlobalBeforeIntercept<TResponseModel>([NotNullWhen(true)] out IApiResponse<TResponseModel>? responseResult)
{
responseResult = null;
#region NetworkAccess
if (!IsConnected)
{
responseResult = ApiResponse.Code<TResponseModel>(ApiResponseCode.NetworkConnectionInterruption, Constants.NetworkConnectionInterruption);
}
#endregion
if (responseResult != null && !responseResult.IsSuccess)
{
ShowResponseErrorMessage(responseResult);
}
return responseResult != null;
}
async Task<IApiResponse<TResponseModel>> SendCoreAsync<TRequestModel, TResponseModel>(
bool isApi,
CancellationToken cancellationToken,
HttpMethod method,
string requestUri,
TRequestModel? requestModel,
bool responseContentMaybeNull)
{
#region ModelValidator
if (isApi && requestModel != null && typeof(TRequestModel) != typeof(object))
{
if (!validator.Validate(requestModel, out var errorMessage))
{
return ApiResponse.Code<TResponseModel>(
ApiResponseCode.RequestModelValidateFail, errorMessage);
}
}
#endregion
if (GlobalBeforeIntercept<TResponseModel>(out var globalBeforeInterceptResponse))
{
return globalBeforeInterceptResponse;
}
IApiResponse<TResponseModel> responseResult;
try
{
using var request = new HttpRequestMessage(method, requestUri)
{
Content = GetRequestContent(
Serializable.ImplType.MessagePack,
requestModel,
cancellationToken),
};
await SetRequestHeaderAuthorization(request);
using var response = await client.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken)
.ConfigureAwait(false);
var code = (ApiResponseCode)response.StatusCode;
if (response.Content == null)
{
responseResult = ApiResponse.Code<TResponseModel>(code);
}
else
{
if (!isApi && typeof(TResponseModel) == typeof(byte[]))
{
var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
responseResult = ApiResponse.Code(code,
content: (TResponseModel)(object)bytes);
}
else if (!isApi && typeof(TResponseModel) == typeof(string))
{
var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
responseResult = ApiResponse.Code(code,
content: (TResponseModel)(object)str);
}
else
{
var mime = response.Content.Headers.ContentType.MediaType;
switch (mime)
{
case MediaTypeNames.JSON:
{
using var stream = await response.Content
.ReadAsStreamAsync().ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
using var json = new JsonTextReader(reader);
responseResult = ApiResponse
.Deserialize<TResponseModel>(jsonSerializer.Value, json);
}
break;
case MediaTypeNames.MessagePack:
{
using var stream = await response.Content
.ReadAsStreamAsync().ConfigureAwait(false);
responseResult = await ApiResponse
.DeserializeAsync<TResponseModel>(stream, cancellationToken);
}
break;
default:
responseResult = ApiResponse.Code<TResponseModel>(ApiResponseCode.UnsupportedResponseMediaType);
break;
}
}
}
}
catch (Exception ex)
{
logger.LogError(ex, nameof(ApiConnection));
responseResult = ApiResponse.Code<TResponseModel>(GetCodeByException(ex));
}
await GlobalResponseIntercept(
isApi,
method,
requestUri,
requestModel,
responseResult,
responseContentMaybeNull);
return responseResult;
}
const int bufferSize = 4096;
public async Task<IApiResponse> DownloadAsync(
CancellationToken cancellationToken,
string requestUri,
string cacheFilePath,
IProgress<float> progress)
{
if (GlobalBeforeIntercept<object>(out var globalBeforeInterceptResponse))
{
return globalBeforeInterceptResponse;
}
HttpMethod method = HttpMethod.Get;
IApiResponse responseResult;
try
{
using var request = new HttpRequestMessage(method, requestUri);
await SetRequestHeaderAuthorization(request);
using var response = await client.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken)
.ConfigureAwait(false);
var code = (ApiResponseCode)response.StatusCode;
responseResult = ApiResponse.Code(code);
if (responseResult.IsSuccess)
{
var total = response.Content.Headers.ContentLength ?? -1L;
if (total > 0)
{
var canReportProgress = progress != null;
using var fileStream = new FileStream(cacheFilePath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize,
true);
using var stream = await response.Content.ReadAsStreamAsync();
var totalRead = 0L;
var buffer = new byte[bufferSize];
var isMoreToRead = true;
var lastProgressValue = 0f;
do
{
cancellationToken.ThrowIfCancellationRequested();
var readAsyncToken = CancellationTokenSource.
CreateLinkedTokenSource(cancellationToken);
readAsyncToken.CancelAfter(5000);
var read = await stream.ReadAsync(
buffer, 0, buffer.Length, readAsyncToken.Token);
if (read == 0)
{
isMoreToRead = false;
}
else
{
await fileStream.WriteAsync(buffer, 0, read, cancellationToken);
totalRead += read;
if (canReportProgress)
{
var progressValue = MathF.Round((float)totalRead / total, 2, MidpointRounding.AwayFromZero);
if (progressValue != lastProgressValue)
{
progress?.Report(progressValue);
lastProgressValue = progressValue;
}
}
}
} while (isMoreToRead);
}
else
{
responseResult.Code = ApiResponseCode.NoResponseContent;
}
}
}
catch (Exception ex)
{
logger.LogError(ex, nameof(ApiConnection));
responseResult = ApiResponse.Code(GetCodeByException(ex));
}
await GlobalResponseIntercept(
method,
requestUri,
responseResult);
return responseResult;
}
public async Task<IApiResponse<TResponseModel>> SendAsync<TRequestModel, TResponseModel>(CancellationToken cancellationToken, HttpMethod method, string requestUri, TRequestModel? request, bool responseContentMaybeNull = false)
{
var rsp = await SendCoreAsync<TRequestModel, TResponseModel>(
isApi: true,
cancellationToken,
method,
requestUri,
requestModel: request,
responseContentMaybeNull);
return rsp;
}
public async Task<IApiResponse<byte[]>> GetRaw(CancellationToken cancellationToken, string requestUri)
{
var rsp = await SendCoreAsync<object, byte[]>(
isApi: false,
cancellationToken,
HttpMethod.Get,
requestUri,
requestModel: null,
responseContentMaybeNull: false);
return rsp;
}
public async Task<IApiResponse<string>> GetHtml(CancellationToken cancellationToken, string requestUri)
{
var rsp = await SendCoreAsync<object, string>(
isApi: false,
cancellationToken,
HttpMethod.Get,
requestUri,
requestModel: null,
responseContentMaybeNull: false);
return rsp;
}
public async Task<IApiResponse> SendAsync<TRequestModel>(CancellationToken cancellationToken, HttpMethod method, string requestUri, TRequestModel? request)
{
var rsp = await SendCoreAsync<TRequestModel, object>(
isApi: true,
cancellationToken,
method,
requestUri,
requestModel: request,
responseContentMaybeNull: true);
return rsp;
}
public async Task<IApiResponse> SendAsync(CancellationToken cancellationToken, HttpMethod method, string requestUri)
{
var rsp = await SendCoreAsync<object, object>(
isApi: true,
cancellationToken,
method,
requestUri,
requestModel: null,
responseContentMaybeNull: true);
return rsp;
}
public async Task<IApiResponse<TResponseModel>> SendAsync<TResponseModel>(CancellationToken cancellationToken, HttpMethod method, string requestUri, bool responseContentMaybeNull = false)
{
var rsp = await SendCoreAsync<object, TResponseModel>(
isApi: true,
cancellationToken,
method,
requestUri,
requestModel: null,
responseContentMaybeNull);
return rsp;
}
}
}
@@ -0,0 +1,18 @@
using System.Application.Models;
namespace System.Application.Services.CloudService
{
internal sealed class ApiResponseCodeException : Exception
{
public ApiResponseCodeException(ApiResponseCode code) : base(ApiResponse.GetMessage(code))
=> Code = code;
public ApiResponseCodeException(ApiResponseCode code, string message) : base(message)
=> Code = code;
public ApiResponseCodeException(ApiResponseCode code, string message, Exception innerException) : base(message, innerException)
=> Code = code;
public ApiResponseCode Code { get; }
}
}
@@ -0,0 +1,12 @@
namespace System.Application.Services.CloudService.Clients.Abstractions
{
internal abstract class ApiClient
{
protected readonly IApiConnection conn;
public ApiClient(IApiConnection conn)
{
this.conn = conn;
}
}
}
@@ -0,0 +1,10 @@
using System.Application.Models;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService.Clients.Abstractions
{
public interface IAccountClient
{
Task<IApiResponse<LoginOrRegisterResponse>> LoginOrRegister(LoginOrRegisterRequest request);
}
}
@@ -0,0 +1,10 @@
using System.Application.Models;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService.Clients.Abstractions
{
public interface IAuthMessageClient
{
ValueTask<IApiResponse> SendSms(SendSmsRequest request);
}
}
@@ -0,0 +1,22 @@
using System.Application.Models;
using System.Application.Services.CloudService.Clients.Abstractions;
using System.Net.Http;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService.Clients
{
internal sealed class AccountClient : ApiClient, IAccountClient
{
public AccountClient(IApiConnection conn) : base(conn)
{
}
public Task<IApiResponse<LoginOrRegisterResponse>> LoginOrRegister(LoginOrRegisterRequest request)
=> conn.SendAsync<LoginOrRegisterRequest, LoginOrRegisterResponse>(
method: HttpMethod.Post,
requestUri: "api/Account/LoginOrRegister",
request: request,
cancellationToken: default,
responseContentMaybeNull: false);
}
}
@@ -0,0 +1,28 @@
using System.Application.Models;
using System.Application.Services.CloudService.Clients.Abstractions;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService.Clients
{
internal sealed class AuthMessageClient : ApiClient, IAuthMessageClient
{
readonly IDictionary<string, DateTime> pairs;
public AuthMessageClient(IApiConnection conn) : base(conn)
{
pairs = AuthMessageClientHelper.Create();
}
Task<IApiResponse> SendSmsCore(SendSmsRequest request)
=> conn.SendAsync(
method: HttpMethod.Post,
requestUri: "api/AuthMessage/SendSms",
request: request,
cancellationToken: default);
public ValueTask<IApiResponse> SendSms(SendSmsRequest request)
=> AuthMessageClientHelper.SendSms(pairs, request, SendSmsCore);
}
}
@@ -0,0 +1,87 @@
using System.Application.Models;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService.Clients
{
public static class AuthMessageClientHelper
{
static IEnumerable<string> QueryClearKeys(IDictionary<string, DateTime> pairs, DateTime now)
{
foreach (var item in pairs)
{
if (!((now - item.Value).TotalSeconds <= Constants.SMSIntervalActually))
{
yield return item.Key;
}
}
}
static void Clear(IDictionary<string, DateTime> pairs, DateTime now)
{
var keys = QueryClearKeys(pairs, now).ToArray();
foreach (var item in keys)
{
pairs.Remove(item);
}
}
/// <summary>
/// 检查在已发送的时间间隔内,则不调用http,直接返回OK
/// </summary>
/// <param name="pairs"></param>
/// <param name="mark"></param>
/// <returns></returns>
static bool CheckInterval(DateTime now, IDictionary<string, DateTime> pairs, string mark)
{
Clear(pairs, now);
if (pairs.TryGetValue(mark, out var value))
{
if ((now - value).TotalSeconds <= Constants.SMSIntervalActually)
{
return true;
}
}
return false;
}
static string GetMark(SendSmsRequest request)
=> request.PhoneNumber + request.Type;
static void SetMark(DateTime now, IDictionary<string, DateTime> pairs, string mark)
{
if (pairs.ContainsKey(mark))
{
pairs[mark] = now;
}
else
{
pairs.Add(mark, now);
}
}
public static IDictionary<string, DateTime> Create()
=> new ConcurrentDictionary<string, DateTime>();
public static async ValueTask<IApiResponse> SendSms(
IDictionary<string, DateTime> pairs,
SendSmsRequest request,
Func<SendSmsRequest, Task<IApiResponse>> func)
{
var now = DateTime.Now;
var mark = GetMark(request);
if (CheckInterval(now, pairs, mark))
{
return ApiResponse.Ok();
}
var r = await func(request);
if (r.IsSuccess)
{
SetMark(now, pairs, mark);
}
return r;
}
}
}
@@ -0,0 +1,73 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Application.Columns;
using System.Application.Models;
using System.Application.Services.CloudService.Clients;
using System.Application.Services.CloudService.Clients.Abstractions;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService
{
public abstract class CloudServiceClientBase : ICloudServiceClient, IApiConnectionPlatformHelper
{
//const string DefaultApiBaseUrl = Constants.Prefix_HTTPS + "api.待定.com";
static string DefaultApiBaseUrl => throw new NotImplementedException();
#region Clients
public IAccountClient Account { get; }
public IAuthMessageClient AuthMessage { get; }
#endregion
protected readonly ILogger logger;
readonly ApiConnection connection;
protected readonly IAuthHelper authHelper;
public string ApiBaseUrl { get; }
public CloudServiceClientBase(
ILogger logger,
IAuthHelper authHelper,
IOptions<ICloudServiceSettings> options,
IModelValidator validator)
{
this.logger = logger;
this.authHelper = authHelper;
var client = CreateHttpClient();
ApiBaseUrl = string.IsNullOrWhiteSpace(options.Value.ApiBaseUrl) ? DefaultApiBaseUrl : options.Value.ApiBaseUrl;
client.BaseAddress = new Uri(ApiBaseUrl);
client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
client.DefaultRequestHeaders.Add(Constants.HeaderAppVersion,
options.Value.AppVersion.ToStringN());
connection = new ApiConnection(logger, client, this, validator);
#region SetClients
Account = new AccountClient(connection);
AuthMessage = new AuthMessageClient(connection);
#endregion
}
protected virtual HttpClient CreateHttpClient() => new HttpClient();
/// <summary>
/// 用户代理
/// <para>https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/User-Agent</para>
/// </summary>
protected virtual string UserAgent => Constants.DefaultUserAgent;
IAuthHelper IApiConnectionPlatformHelper.Auth => authHelper;
public abstract Task SaveAuthTokenAsync(string authToken);
public abstract Task OnLoginedAsync(IReadOnlyPhoneNumber? phoneNumber, ILoginResponse response);
public abstract void ShowResponseErrorMessage(string message);
public abstract (string filePath, string mime)? TryHandleUploadFile(Stream imageFileStream, UploadFileType uploadFileType);
}
}
@@ -0,0 +1,100 @@
using System.Application.Models;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService
{
public interface IApiConnection
{
/// <summary>
/// 获取服务端接口的原始内容(ByteArray)
/// </summary>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <returns></returns>
Task<IApiResponse<byte[]>> GetRaw(CancellationToken cancellationToken, string requestUri);
/// <summary>
/// 获取服务端接口的HTML内容(String)
/// </summary>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <returns></returns>
Task<IApiResponse<string>> GetHtml(CancellationToken cancellationToken, string requestUri);
/// <summary>
/// 下载服务端接口内容
/// <para>注意事项:在完成后根据是否成功以及业务需求需要调用 progress 报告进度满值</para>
/// </summary>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <param name="cacheFilePath">缓存文件路径</param>
/// <param name="progress">进度报告</param>
/// <returns></returns>
Task<IApiResponse> DownloadAsync(CancellationToken cancellationToken, string requestUri, string cacheFilePath, IProgress<float> progress);
#region SendAsync
/// <summary>
/// RequestModel+ResponseModel(调用服务端接口)
/// </summary>
/// <typeparam name="TRequestModel">请求模型类型</typeparam>
/// <typeparam name="TResponseModel">响应模型类型</typeparam>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="method">HTTP方法</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <param name="request">请求模型</param>
/// <param name="responseContentMaybeNull">响应内容是否能为<see langword="null"/></param>
/// <returns></returns>
Task<IApiResponse<TResponseModel>> SendAsync<TRequestModel, TResponseModel>(
CancellationToken cancellationToken,
HttpMethod method,
string requestUri,
TRequestModel? request,
bool responseContentMaybeNull = false);
/// <summary>
/// RequestModel(调用服务端接口)
/// </summary>
/// <typeparam name="TRequestModel">请求模型类型</typeparam>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="method">HTTP方法</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <param name="request">请求模型</param>
/// <returns></returns>
Task<IApiResponse> SendAsync<TRequestModel>(
CancellationToken cancellationToken,
HttpMethod method,
string requestUri,
TRequestModel? request);
/// <summary>
/// NoModel(调用服务端接口)
/// </summary>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="method">HTTP方法</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <returns></returns>
Task<IApiResponse> SendAsync(
CancellationToken cancellationToken,
HttpMethod method,
string requestUri);
/// <summary>
/// ResponseModel(调用服务端接口)
/// </summary>
/// <typeparam name="TResponseModel">响应模型类型</typeparam>
/// <param name="cancellationToken">传播应取消操作的通知</param>
/// <param name="method">HTTP方法</param>
/// <param name="requestUri">服务端接口URL地址</param>
/// <returns></returns>
Task<IApiResponse<TResponseModel>> SendAsync<TResponseModel>(
CancellationToken cancellationToken,
HttpMethod method,
string requestUri,
bool responseContentMaybeNull = false);
#endregion
}
}
@@ -0,0 +1,42 @@
using System.Application.Columns;
using System.Application.Models;
using System.IO;
using System.Threading.Tasks;
namespace System.Application.Services.CloudService
{
public interface IApiConnectionPlatformHelper
{
#region Authentication
IAuthHelper Auth { get; }
/// <summary>
/// 保存用户登录凭证
/// </summary>
/// <param name="authToken"></param>
Task SaveAuthTokenAsync(string authToken);
/// <summary>
/// 当登录完成时
/// </summary>
/// <param name="phoneNumber"></param>
/// <param name="response"></param>
Task OnLoginedAsync(IReadOnlyPhoneNumber? phoneNumber, ILoginResponse response);
#endregion
/// <summary>
/// 显示响应错误消息
/// </summary>
/// <param name="message"></param>
void ShowResponseErrorMessage(string message);
/// <summary>
/// 尝试将需要[上传的文件流]处理(文件压缩,格式转换[heic->png],由客户端平台原生实现)保存到临时路径中并返回信息
/// </summary>
/// <param name="imageStream"></param>
/// <returns></returns>
(string filePath, string mime)? TryHandleUploadFile(Stream imageFileStream, UploadFileType uploadFileType);
}
}
@@ -0,0 +1,34 @@
using System.Application.Models;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
namespace System.Application.Services.CloudService
{
internal sealed class UploadFileContent : StreamContent
{
readonly IUploadFileSource? uploadFile;
UploadFileContent(Stream stream) : base(stream)
{
Headers.ContentLength = stream.Length;
}
public UploadFileContent(IUploadFileSource uploadFile, Stream stream) : this(stream)
{
this.uploadFile = uploadFile;
if (string.IsNullOrEmpty(uploadFile.MIME))
throw new NullReferenceException(nameof(uploadFile.MIME));
Headers.ContentType = new MediaTypeHeaderValue(uploadFile.MIME);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
uploadFile?.Dispose();
}
base.Dispose(disposing);
}
}
}
@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace System.Application.Services
{
public interface IAuthHelper
{
ValueTask<string?> GetAuthTokenAsync();
/// <summary>
/// 登出
/// </summary>
Task SignOutAsync();
}
}

Some files were not shown because too many files have changed in this diff Show More