Compare commits

...
44 Commits
Author SHA1 Message Date
rmbgame 0509d2b843 ready 1.1.4 2021-02-23 02:34:20 +08:00
rmbgame 8aa450f25f ready 1.1.4 2021-02-23 02:33:41 +08:00
rmbgame 81aaa50508 修改更新日志从网络请求数据而不是写在程序内 2021-02-23 02:14:16 +08:00
rmbgame 927b652c42 添加steam更新的代理服务,解决部分用户steam更新无法连接网络的问题 2021-02-22 19:37:56 +08:00
rmbgame 4de1ac13f0 添加windows系统代理方式 2021-02-22 17:25:49 +08:00
Aigio Liu 46de667c17 添加Win7疑难解答程序
优化发布配置
优化电话服务
2021-02-22 17:00:10 +08:00
rmbgame 9d14dce321 Merge branch 'develop' of https://github.com/rmbadmin/SteamTools into develop 2021-02-22 03:02:15 +08:00
rmbgame ea33fa3444 优化反代速度
修复steam图片反代启动时创意工坊图片显示不了的问题
修复uplay更新反代启动时更新失败的问题
2021-02-22 03:00:13 +08:00
Aigio Liu 9b980427f6 fix 2021-02-22 01:02:42 +08:00
Aigio Liu 451d707626 Merge branch 'develop' of https://github.com/rmbadmin/SteamTools into develop 2021-02-22 01:02:24 +08:00
Aigio Liu f483a5dcba 优化 MSBuild 在 Release 时 不生成PDB
新增平台架构(win/mac/linux)
优化 发布配置
2021-02-22 01:02:19 +08:00
rmbgame 31c4111495 修复成就图标加载失败 2021-02-22 00:39:53 +08:00
rmbgame f0a14e332d ready 1.1.3 2021-02-21 21:39:02 +08:00
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 948016c84f ready 1.1.2 2021-01-29 19:10:07 +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 98da730c18 push 1.1.1 2021-01-23 00:02:52 +08:00
rmbgame ab6bfd45b7 ready 1.0.6 2021-01-12 16:25:19 +08:00
rmbgame 8abfc81ba2 ready 1.0.5 2021-01-08 09:57:34 +08:00
rmbgame d4d6c2fb51 update readme 2020-12-29 03:26:07 +08:00
rmbgame 8da01ac3dd ready 1.0.4 2020-12-28 20:35:45 +08:00
rmbgame 2ab26f7848 ready 1.0.3 2020-12-26 22:20:50 +08:00
rmbgame d646eacda1 ready 1.0.2 2020-12-21 21:54:23 +08:00
rmbgame fb029f729b ready 1.0.0 2020-12-21 21:47:08 +08:00
rmbgame 5ef6663269 delete ref lib 2020-12-21 21:36:17 +08:00
rmbgame 5c86b53000 ready 1.0.0 2020-12-17 12:10:47 +08:00
rmbgame efb6adbd9b delete ref lib 2020-12-16 10:38:03 +08:00
rmbgame 209202b5cf sync develop 2020-12-16 10:36:52 +08:00
rmbgame c0a081e226 delete ref lib 2020-12-16 10:34:52 +08:00
439 changed files with 48852 additions and 1525 deletions
+16
View File
@@ -0,0 +1,16 @@
<Project>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<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>
+71 -75
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,82 +11,63 @@
[![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>
[English](https://github.com/rmbadmin/SteamTools/blob/develop/README.en.md)
English | [简体中文](./README.md)
[简体中文](https://github.com/rmbadmin/SteamTools/blob/develop/README.md)
## ✨ 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.
## 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)
------
<!--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)-->
## 🖥 Operating Environment
- Windows 10 1607+
- Windows 8.1 / Windows Server 2012 R2
- Windows 7 SP1 [ESU](https://docs.microsoft.com/troubleshoot/windows-client/windows-7-eos-faq/windows-7-extended-security-updates-faq)
- Prerequisites
- 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 10.0+~~
-->
## Core Functions
## 🌎 Roadmap
Read what we [milestones](https://github.com/SteamTools-Team/SteamTools/milestones), and feel free to ask questions.
### 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
## ⌨️ 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)
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.
![](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)
---
### 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)
---
### 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)
@@ -104,4 +84,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 -80
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,87 +11,73 @@
[![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>
[English](https://github.com/rmbadmin/SteamTools/blob/develop/README.en.md)
<div align="center"><img src="./resources/screenshots.jpg" /></div>
[简体中文](https://github.com/rmbadmin/SteamTools/blob/develop/README.md)
[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 屏蔽
## 效果展示
-------
![](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/e-1.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/t.png)
------
<!--发布配置SelfContained=true时会自动打包VC++相关程序集-->
<!--先决条件 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 10 1607+
- Windows 8.1 / Windows Server 2012 R2
- Windows 7 SP1 [ESU](https://docs.microsoft.com/zh-cn/troubleshoot/windows-client/windows-7-eos-faq/windows-7-extended-security-updates-faq)
- 先决条件
- 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 10.0+~~
-->
## 核心功能
## 🌎 路线图
查看这个 [milestones](https://github.com/SteamTools-Team/SteamTools/milestones) 来了解我们下一步的开发计划,并随时提出问题。
### 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账号
## ⌨️ 开发环境
[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)
该功能是读取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)
---
### 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)
本地令牌交易市场报价确认
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/4-1.png)
---
### 5. 一些游戏工具
目前已有强制游戏无边框窗口化,CSGO修复VAC屏蔽。
这一块是随缘做一些我经常用或者闲着没事捣鼓的功能。
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/5.png)
![](https://raw.githubusercontent.com/rmbadmin/SteamTools/develop/resources/5-1.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)
@@ -110,4 +94,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)
+193
View File
@@ -0,0 +1,193 @@
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.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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop.Platform", "System.Application.SteamTools.Client.Desktop.Platform\System.Application.SteamTools.Client.Desktop.Platform.csproj", "{DD2973AE-D1FC-4307-AFE2-EE2945183D3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop.Platform.Linux", "System.Application.SteamTools.Client.Desktop.Platform.Linux\System.Application.SteamTools.Client.Desktop.Platform.Linux.csproj", "{D4CF08F5-8EAB-48FB-AAA3-1A221F7B2A82}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop.Platform.Mac", "System.Application.SteamTools.Client.Desktop.Platform.Mac\System.Application.SteamTools.Client.Desktop.Platform.Mac.csproj", "{948C1EA8-12DB-4F8A-A190-8615DFBD9FE4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Application.SteamTools.Client.Desktop.Platform.Windows", "System.Application.SteamTools.Client.Desktop.Platform.Windows\System.Application.SteamTools.Client.Desktop.Platform.Windows.csproj", "{141532E1-540E-4E50-8723-96936C191281}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Win7Troubleshoot", "Win7Troubleshoot\Win7Troubleshoot.csproj", "{60530AF8-4790-4A85-BCE2-C5673E9C601B}"
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
{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
{DD2973AE-D1FC-4307-AFE2-EE2945183D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD2973AE-D1FC-4307-AFE2-EE2945183D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD2973AE-D1FC-4307-AFE2-EE2945183D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD2973AE-D1FC-4307-AFE2-EE2945183D3D}.Release|Any CPU.Build.0 = Release|Any CPU
{D4CF08F5-8EAB-48FB-AAA3-1A221F7B2A82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4CF08F5-8EAB-48FB-AAA3-1A221F7B2A82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4CF08F5-8EAB-48FB-AAA3-1A221F7B2A82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4CF08F5-8EAB-48FB-AAA3-1A221F7B2A82}.Release|Any CPU.Build.0 = Release|Any CPU
{948C1EA8-12DB-4F8A-A190-8615DFBD9FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{948C1EA8-12DB-4F8A-A190-8615DFBD9FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{948C1EA8-12DB-4F8A-A190-8615DFBD9FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{948C1EA8-12DB-4F8A-A190-8615DFBD9FE4}.Release|Any CPU.Build.0 = Release|Any CPU
{141532E1-540E-4E50-8723-96936C191281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{141532E1-540E-4E50-8723-96936C191281}.Debug|Any CPU.Build.0 = Debug|Any CPU
{141532E1-540E-4E50-8723-96936C191281}.Release|Any CPU.ActiveCfg = Release|Any CPU
{141532E1-540E-4E50-8723-96936C191281}.Release|Any CPU.Build.0 = Release|Any CPU
{60530AF8-4790-4A85-BCE2-C5673E9C601B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60530AF8-4790-4A85-BCE2-C5673E9C601B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60530AF8-4790-4A85-BCE2-C5673E9C601B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60530AF8-4790-4A85-BCE2-C5673E9C601B}.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}
{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}
{DD2973AE-D1FC-4307-AFE2-EE2945183D3D} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{D4CF08F5-8EAB-48FB-AAA3-1A221F7B2A82} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{948C1EA8-12DB-4F8A-A190-8615DFBD9FE4} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{141532E1-540E-4E50-8723-96936C191281} = {B43459A1-709B-4623-9677-AAC2FEE4FBA3}
{60530AF8-4790-4A85-BCE2-C5673E9C601B} = {1460800B-D9ED-4147-B1E7-ABF502BEF59A}
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,21 @@
<?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>
<DefineConstants>PUBLISH;$(DefineConstants)</DefineConstants>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish\linux-x64</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,21 @@
<?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>
<DefineConstants>PUBLISH;$(DefineConstants)</DefineConstants>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish\osx-x64</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,31 @@
<?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>
<DefineConstants>PUBLISH;$(DefineConstants)</DefineConstants>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish\win-x64</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>true</PublishReadyToRun>
<!--可以通过将应用程序集编译为 ReadyToRun (R2R) 格式来改进 .NET Core 应用程序的启动时间和延迟。 R2R 是一种预先 (AOT) 编译形式 https://docs.microsoft.com/zh-cn/dotnet/core/deploying/ready-to-run-->
<PublishTrimmed>True</PublishTrimmed>
<TrimMode>link</TrimMode>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<!--剪裁框架库功能 https://docs.microsoft.com/zh-cn/dotnet/core/deploying/trimming-options#trimming-framework-library-features-->
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<EventSourceSupport>false</EventSourceSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<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,77 @@
<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.Platform\System.Application.SteamTools.Client.Desktop.Platform.csproj" />
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop\System.Application.SteamTools.Client.Desktop.csproj" />
<ProjectReference Include="..\System.Common.CoreLib\System.Common.CoreLib.csproj" />
</ItemGroup>
<ItemGroup Condition=" $(RuntimeIdentifier.Contains(`win`)) Or ('$(OS)' == 'Windows_NT' And !$(DefineConstants.Contains(`PUBLISH`))) ">
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop.Platform.Windows\System.Application.SteamTools.Client.Desktop.Platform.Windows.csproj" />
</ItemGroup>
<ItemGroup Condition=" $(RuntimeIdentifier.Contains(`osx`)) Or ('$(OS)' == 'OSX' And !$(DefineConstants.Contains(`PUBLISH`))) ">
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop.Platform.Mac\System.Application.SteamTools.Client.Desktop.Platform.Mac.csproj" />
</ItemGroup>
<ItemGroup Condition=" $(RuntimeIdentifier.Contains(`linux`)) Or ('$(OS)' == 'Unix' And !$(DefineConstants.Contains(`PUBLISH`))) ">
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop.Platform.Linux\System.Application.SteamTools.Client.Desktop.Platform.Linux.csproj" />
</ItemGroup>
<ItemGroup>
<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,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,97 @@
using Avalonia.Input;
using System.Collections.Generic;
namespace System.Application.UI
{
/// <summary>
/// 拖拽功能工具类
/// https://github.com/AvaloniaUI/Avalonia/blob/0.10.0-preview6/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
/// </summary>
public static class DragDropUtil
{
public static void InitDragDrop(this IOnDragDropListener listener,
InputElement element)
{
element.PointerPressed += async (_, e)
=> await DragDrop.DoDragDrop(e, new DataObject(), DragDropEffects.Copy);
element.AddHandler(DragDrop.DropEvent, Drop);
element.AddHandler(DragDrop.DragOverEvent, DragOver);
void DragOver(object? s, DragEventArgs? e)
{
if (e?.Data == null) return;
listener.DragOver(s is InputElement el ? el : element, e);
}
void Drop(object? s, DragEventArgs? e)
{
if (e?.Data == null) return;
listener.Drop(s is InputElement el ? el : element, e);
}
}
public static void InitDragDrop(this IOnDragDropListener listener,
IEnumerable<InputElement> elements)
{
foreach (var element in elements)
{
listener.InitDragDrop(element);
}
}
public static void InitDragDrop(this IOnDragDropListener listener,
params InputElement[] elements)
{
IEnumerable<InputElement> elements_ = elements;
listener.InitDragDrop(elements_);
}
public interface IOnDragDropListener
{
void DragOver(InputElement sender, DragEventArgs e)
{
// Only allow Copy or Link as Drop Operations.
e.DragEffects &= (DragDropEffects.Copy | DragDropEffects.Link);
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames))
e.DragEffects = DragDropEffects.None;
}
void Drop(InputElement sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Text))
{
var text = e.Data.GetText();
if (text != null)
{
DropText(sender, text);
}
}
else if (e.Data.Contains(DataFormats.FileNames))
{
var fileNames = e.Data.GetFileNames();
if (fileNames != null)
{
DropFileNames(sender, fileNames);
}
}
}
/// <summary>
/// 拖拽文本
/// </summary>
/// <param name="sender"></param>
/// <param name="text"></param>
void DropText(InputElement sender, string text);
/// <summary>
/// 拖拽路径,路径可能是文件也可能是文件夹
/// </summary>
/// <param name="sender"></param>
/// <param name="fileNames"></param>
void DropFileNames(InputElement sender, IEnumerable<string> fileNames);
}
}
}
@@ -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,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>System</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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.0" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0" />
<PackageReference Include="MessageBox.Avalonia" Version="1.0.5">
<Aliases>MessageBox_Avalonia</Aliases>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,113 @@
extern alias MessageBox_Avalonia;
using Avalonia.Controls;
using MessageBox_Avalonia::MessageBox.Avalonia;
using MessageBox_Avalonia::MessageBox.Avalonia.BaseWindows.Base;
using MessageBox_Avalonia::MessageBox.Avalonia.DTO;
using MessageBox_Avalonia::MessageBox.Avalonia.Enums;
using System.Reflection;
using System.Threading.Tasks;
// ReSharper disable once CheckNamespace
namespace System.Windows
{
/// <summary>
/// 显示消息框。
/// </summary>
public static class MessageBox
{
static ButtonEnum GetButtonEnum(this MessageBoxButton button) => button switch
{
MessageBoxButton.OK => ButtonEnum.Ok,
MessageBoxButton.OKCancel => ButtonEnum.OkCancel,
MessageBoxButton.YesNo => ButtonEnum.YesNo,
MessageBoxButton.YesNoCancel => ButtonEnum.YesNoCancel,
#pragma warning disable CS0618 // 类型或成员已过时
MessageBoxButton.OkAbort => ButtonEnum.OkAbort,
MessageBoxButton.YesNoAbort => ButtonEnum.YesNoAbort,
#pragma warning restore CS0618 // 类型或成员已过时
_ => throw new ArgumentOutOfRangeException(nameof(button), $"value: {button}"),
};
static Icon GetIcon(this MessageBoxImage icon) => icon switch
{
MessageBoxImage.Asterisk => Icon.Info,
MessageBoxImage.Error => Icon.Error,
MessageBoxImage.Exclamation => Icon.Warning,
MessageBoxImage.None => Icon.None,
#pragma warning disable CS0618 // 类型或成员已过时
MessageBoxImage.Question => Icon.Info,
MessageBoxImage.Battery => Icon.Battery,
MessageBoxImage.Database => Icon.Database,
MessageBoxImage.Folder => Icon.Folder,
MessageBoxImage.Forbidden => Icon.Forbidden,
MessageBoxImage.Plus => Icon.Plus,
MessageBoxImage.Setting => Icon.Setting,
MessageBoxImage.SpeakerLess => Icon.SpeakerLess,
MessageBoxImage.SpeakerMore => Icon.SpeakerMore,
MessageBoxImage.Stop2 => Icon.Stop,
MessageBoxImage.Stopwatch => Icon.Stopwatch,
MessageBoxImage.Wifi => Icon.Wifi,
#pragma warning restore CS0618 // 类型或成员已过时
_ => throw new ArgumentOutOfRangeException(nameof(icon), $"value: {icon}"),
};
static MessageBoxResult GetResult(this ButtonResult result) => result switch
{
ButtonResult.Ok => MessageBoxResult.OK,
ButtonResult.Yes => MessageBoxResult.Yes,
ButtonResult.No => MessageBoxResult.No,
#pragma warning disable CS0618 // 类型或成员已过时
ButtonResult.Abort => MessageBoxResult.Abort,
#pragma warning restore CS0618 // 类型或成员已过时
ButtonResult.Cancel => MessageBoxResult.Cancel,
ButtonResult.None => MessageBoxResult.None,
_ => throw new ArgumentOutOfRangeException(nameof(result), $"value: {result}"),
};
/// <summary>
/// 显示一个消息框,该消息框包含消息、标题栏标题、按钮和图标,并且返回结果。
/// </summary>
/// <param name="messageBoxText">一个 <see cref="string"/>,用于指定要显示的文本。</param>
/// <param name="caption">一个 <see cref="string"/>,用于指定要显示的标题栏标题。</param>
/// <param name="button">一个 <see cref="MessageBoxButton"/> 值,用于指定要显示哪个按钮或哪些按钮。</param>
/// <param name="icon">一个 <see cref="MessageBoxImage"/> 值,用于指定要显示的图标。</param>
/// <returns>一个 <see cref="MessageBoxResult"/> 值,用于指定用户单击了哪个消息框按钮。</returns>
public static async Task<MessageBoxResult> ShowAsync(
string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
{
var parms = new MessageBoxStandardParams
{
ButtonDefinitions = button.GetButtonEnum(),
ContentTitle = caption,
ContentMessage = messageBoxText,
Icon = icon.GetIcon(),
Style = Style.None,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
};
var msBoxStandardWindow = MessageBoxManager.GetMessageBoxStandardWindow(parms);
var window = GetWindow(msBoxStandardWindow);
if (window != null)
{
window.Topmost = true;
window.SetResizeMode(ResizeMode.NoResize);
window.SetDefaultFontFamily();
}
var result = await msBoxStandardWindow.Show();
return GetResult(result);
}
static Window? GetWindow(IMsBoxWindow<ButtonResult> window)
{
var _window = window.GetType().GetField("_window", BindingFlags.NonPublic | BindingFlags.Instance);
if (_window != null && _window.FieldType.IsSubclassOf(typeof(Window)))
{
if (_window.GetValue(window) is Window w)
{
return w;
}
}
return null;
}
}
}
@@ -0,0 +1,19 @@
// ReSharper disable once CheckNamespace
namespace System.Windows
{
/// <summary>
/// 指定显示在消息框上的按钮。 用作 <see cref="MessageBox"/>.Show... 方法的参数。
/// </summary>
public enum MessageBoxButton
{
OK = 0,
OKCancel = 1,
YesNo = 4,
YesNoCancel = 3,
[Obsolete("non-standard api")]
OkAbort = 1000,
[Obsolete("non-standard api")]
YesNoAbort = 1001,
}
}
@@ -0,0 +1,45 @@
// ReSharper disable once CheckNamespace
namespace System.Windows
{
/// <summary>
/// 指定消息框所显示的图标。
/// </summary>
public enum MessageBoxImage
{
#pragma warning disable CA1069 // 不应复制枚举值
Asterisk = 64,
Error = 16,
Exclamation = 48,
Hand = 16,
Information = 64,
None = 0,
[Obsolete("The question mark message icon is no longer recommended because it does not clearly represent a specific type of message and because the phrasing of a message as a question could apply to any message type. In addition, users can confuse the question mark symbol with a help information symbol. Therefore, do not use this question mark symbol in your message boxes. The system continues to support its inclusion only for backward compatibility.")]
Question = 32,
Stop = 16,
Warning = 48,
[Obsolete("non-standard api")]
Battery = 1000,
[Obsolete("non-standard api")]
Database,
[Obsolete("non-standard api")]
Folder,
[Obsolete("non-standard api")]
Forbidden,
[Obsolete("non-standard api")]
Plus,
[Obsolete("non-standard api")]
Setting,
[Obsolete("non-standard api")]
SpeakerLess,
[Obsolete("non-standard api")]
SpeakerMore,
[Obsolete("non-standard api")]
Stop2,
[Obsolete("non-standard api")]
Stopwatch,
[Obsolete("non-standard api")]
Wifi,
#pragma warning restore CA1069 // 不应复制枚举值
}
}
@@ -0,0 +1,18 @@
// ReSharper disable once CheckNamespace
namespace System.Windows
{
/// <summary>
/// 指定用户单击的消息框按钮。 由 <see cref="MessageBox"/>.Show... 方法返回。
/// </summary>
public enum MessageBoxResult
{
Cancel = 2,
No = 7,
None = 0,
OK = 1,
Yes = 6,
[Obsolete("non-standard api")]
Abort = 1000,
}
}
@@ -0,0 +1,32 @@
using System.Application.Services;
// ReSharper disable once CheckNamespace
namespace System.Windows
{
/// <summary>
/// 指定是否可以调整窗口大小,并在可调整大小的情况下指定如何调整窗口大小。由 SetResizeMode 使用。
/// </summary>
public enum ResizeMode
{
/// <summary>
/// 用户无法调整窗口的大小。 不显示 "最大化" 和 "最小化" 框。
/// </summary>
NoResize = IDesktopPlatformService.ResizeMode_NoResize,
/// <summary>
/// 用户只能最小化窗口并从任务栏还原。 同时显示 "最小化" 和 "最大化" 框,但只启用 "最小化" 框。
/// </summary>
CanMinimize = IDesktopPlatformService.ResizeMode_CanMinimize,
/// <summary>
/// 用户可以通过使用 "最小化" 和 "最大化" 框以及围绕窗口的可拖动轮廓来完全调整窗口的大小。 显示和启用 "最小化" 和 "最大化" 框。 (默认) 。
/// </summary>
CanResize = IDesktopPlatformService.ResizeMode_CanResize,
/// <summary>
/// 此选项具有与相同的功能 <see cref="CanResize"/> ,但会将 "调整手柄" 添加到窗口的右下角。
/// </summary>
[Obsolete("not implemented")]
CanResizeWithGrip = IDesktopPlatformService.ResizeMode_CanResizeWithGrip,
}
}
@@ -0,0 +1,75 @@
using System.Application.Services;
using System.IO;
using FontFamily = Avalonia.Media.FontFamily;
using Window = Avalonia.Controls.Window;
// ReSharper disable once CheckNamespace
namespace System.Windows
{
/// <summary>
/// 通过扩展兼容WPF部分API的内容
/// </summary>
public static partial class WpfCompatExtensions
{
static readonly Lazy<bool> lazy_has_msyh = new Lazy<bool>(() =>
{
if (DI.Platform == Platform.Windows)
{
var fontsPath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
var msyhFilePath = Path.Combine(fontsPath, "msyh.ttc");
return File.Exists(msyhFilePath);
}
else
{
return false;
}
});
public static bool HAS_FONT_MSYH => lazy_has_msyh.Value;
public static void SetDefaultFontFamily(this Window window)
{
if (DI.Platform == Platform.Windows)
{
if (HAS_FONT_MSYH) // (版权、许可)不能在非WinOS上使用 微软雅黑字体,不可将字体嵌入程序
{
string name;
var major = Environment.OSVersion.Version.Major;
if (major > 6 || (major == 6 && Environment.OSVersion.Version.Minor >= 2))
{
name = "Microsoft YaHei UI";
}
else
{
name = "Microsoft YaHei";
}
window.FontFamily = new FontFamily(name);
}
}
}
/// <summary>
/// 设置调整大小模式。
/// </summary>
/// <param name="window"></param>
/// <param name="value"></param>
public static void SetResizeMode(this Window window, ResizeMode value)
{
var p = DI.Get<IDesktopPlatformService>();
switch (value)
{
case ResizeMode.NoResize:
case ResizeMode.CanMinimize:
window.CanResize = false;
break;
case ResizeMode.CanResize:
#pragma warning disable CS0618 // 类型或成员已过时
case ResizeMode.CanResizeWithGrip:
#pragma warning restore CS0618 // 类型或成员已过时
window.CanResize = true;
break;
}
p.SetResizeMode(window.PlatformImpl.Handle.Handle, (int)value);
}
}
}
@@ -0,0 +1,10 @@
namespace System.Application.Services.Implementation
{
internal sealed class LinuxDesktopPlatformServiceImpl : IDesktopPlatformService
{
public void SetResizeMode(IntPtr hWnd, int value)
{
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Application.Services;
using System.Application.Services.Implementation;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDesktopPlatformService(this IServiceCollection services)
{
if (DI.Platform == Platform.Linux)
{
services.AddSingleton<IDesktopPlatformService, LinuxDesktopPlatformServiceImpl>();
}
else
{
throw new PlatformNotSupportedException();
}
return services;
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>System.Application.Services</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.Platform\System.Application.SteamTools.Client.Desktop.Platform.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,9 @@
namespace System.Application.Services.Implementation
{
internal sealed class MacDesktopPlatformServiceImpl : IDesktopPlatformService
{
public void SetResizeMode(IntPtr hWnd, int value)
{
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Application.Services;
using System.Application.Services.Implementation;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDesktopPlatformService(this IServiceCollection services)
{
if (DI.Platform == Platform.Apple)
{
services.AddSingleton<IDesktopPlatformService, MacDesktopPlatformServiceImpl>();
}
else
{
throw new PlatformNotSupportedException();
}
return services;
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>System.Application.Services</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.Platform\System.Application.SteamTools.Client.Desktop.Platform.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,90 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace System.Application.Services.Implementation
{
[SupportedOSPlatform("Windows")]
internal sealed class WindowsDesktopPlatformServiceImpl : IDesktopPlatformService
{
#region 窗口右上角的三个按钮(最小化,最大化,关闭)
// https://stackoverflow.com/questions/339620/how-do-i-remove-minimize-and-maximize-from-a-resizable-window-in-wpf
// https://blog.magnusmontin.net/2014/11/30/disabling-or-hiding-the-minimize-maximize-or-close-button-of-a-wpf-window/
public const int MF_BYCOMMAND = 0x00000000;
public const int SC_CLOSE = 0xF060;
public const int SC_MINIMIZE = 0xF020;
public const int SC_MAXIMIZE = 0xF030;
[DllImport("user32.dll")]
public static extern int DeleteMenu(IntPtr hMenu, int nPosition, int wFlags);
[DllImport("user32.dll")]
public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
public const int GWL_STYLE = -16,
WS_MAXIMIZEBOX = 0x10000,
WS_MINIMIZEBOX = 0x20000;
[DllImport("user32.dll")]
public extern static int GetWindowLong(IntPtr hwnd, int index);
[DllImport("user32.dll")]
public extern static int SetWindowLong(IntPtr hwnd, int index, int value);
public const int WS_SYSMENU = 0x80000;
public static void EnableMinimizeButton(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) | WS_MINIMIZEBOX);
}
public static void EnableMaximizeButton(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) | WS_MAXIMIZEBOX);
}
public static void ShowMinimizeAndMaximizeButtons(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) | WS_MAXIMIZEBOX | WS_MINIMIZEBOX);
}
public static void ShowAllButtons(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) | WS_SYSMENU);
}
public static void HideMinimizeAndMaximizeButtons(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
}
public static void DisableMinimizeButton(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_MINIMIZEBOX);
}
public static void DisableMaximizeButton(IntPtr hWnd)
{
_ = SetWindowLong(hWnd, GWL_STYLE, GetWindowLong(hWnd, GWL_STYLE) & ~WS_MAXIMIZEBOX);
}
public void SetResizeMode(IntPtr hWnd, int value)
{
switch (value)
{
case IDesktopPlatformService.ResizeMode_NoResize:
_ = DeleteMenu(GetSystemMenu(hWnd, false), SC_MAXIMIZE, MF_BYCOMMAND);
_ = DeleteMenu(GetSystemMenu(hWnd, false), SC_MINIMIZE, MF_BYCOMMAND);
HideMinimizeAndMaximizeButtons(hWnd);
break;
case IDesktopPlatformService.ResizeMode_CanMinimize:
_ = DeleteMenu(GetSystemMenu(hWnd, false), SC_MAXIMIZE, MF_BYCOMMAND);
DisableMaximizeButton(hWnd);
break;
}
}
#endregion
}
}
@@ -0,0 +1,23 @@
using System;
using System.Application.Services;
using System.Application.Services.Implementation;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDesktopPlatformService(this IServiceCollection services)
{
if (DI.Platform == Platform.Windows)
{
services.AddSingleton<IDesktopPlatformService, WindowsDesktopPlatformServiceImpl>();
}
else
{
throw new PlatformNotSupportedException();
}
return services;
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>System.Application.Services</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.Platform\System.Application.SteamTools.Client.Desktop.Platform.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,14 @@
using ResizeMode = System.Int32;
namespace System.Application.Services
{
public interface IDesktopPlatformService
{
void SetResizeMode(IntPtr hWnd, ResizeMode value);
public const ResizeMode ResizeMode_NoResize = 0;
public const ResizeMode ResizeMode_CanMinimize = 1;
public const ResizeMode ResizeMode_CanResize = 2;
public const ResizeMode ResizeMode_CanResizeWithGrip = 3;
}
}
@@ -0,0 +1,9 @@
namespace System.Application.Services.Implementation
{
public class DesktopPlatformServiceImpl : IDesktopPlatformService
{
public void SetResizeMode(IntPtr hWnd, int value)
{
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>System.Application.Services</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\System.Application.SteamTools.Client.csproj" />
</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,38 @@
<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>
<PackageReference Include="ReactiveUI" Version="13.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.SteamTools.Client.Desktop.Platform\System.Application.SteamTools.Client.Desktop.Platform.csproj" />
<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>

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