mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-08-17 19:31:42 +02:00
Compare commits
158 Commits
v32.1
...
app-json-2
Author | SHA1 | Date | |
---|---|---|---|
9448cd05b6
|
|||
2424a8ac8d
|
|||
e7705587c2
|
|||
bd7904b88e
|
|||
3db599e901
|
|||
69a68b463c
|
|||
b2389dd521
|
|||
610516de1f
|
|||
3a6b83e0ba
|
|||
22958536e7
|
|||
4db8c302d8
|
|||
7e7d140957
|
|||
8c68438fbb
|
|||
f625a39b4d
|
|||
7fd644449c
|
|||
e4a09515b0
|
|||
9ac9f2246f
|
|||
bbc734ba9b
|
|||
6837b05b0d
|
|||
c94808a15f
|
|||
739e87c5ab
|
|||
d463b407f4
|
|||
cd418f4871
|
|||
176a81e055 | |||
![]() |
1cf3e76644 | ||
33f5ab7cce
|
|||
b9a5664740
|
|||
845ac1b0fa
|
|||
1bead42a0e
|
|||
8f1c91b2cc
|
|||
9ae5ece24b
|
|||
053ab5b091
|
|||
71c628fdf8
|
|||
af621b8d46
|
|||
31fe6aed35
|
|||
c25426af55
|
|||
59129ba20a
|
|||
f7bfe052ca
|
|||
c9bb46c8c7
|
|||
73f4c70325
|
|||
de5a8b690b
|
|||
daa2feb445
|
|||
4e94e788bc
|
|||
133ec532d2
|
|||
3d435d0165
|
|||
3e8151e1f3
|
|||
9f98eba9c1
|
|||
6b54a80be1
|
|||
1e6e5c6f92
|
|||
2459c8ee1a
|
|||
d129a60d1c
|
|||
65ecb0177c
|
|||
d51dcb0a84
|
|||
b13b85dedd
|
|||
15e8b9da63
|
|||
9572f0f002
|
|||
2f3b8b974c
|
|||
bff86b09c7
|
|||
5ca7cf09e8
|
|||
a1c93232d0
|
|||
db5f9d65db
|
|||
4cbf387e2a
|
|||
64cf3c9fbb
|
|||
a4ebd5eed6
|
|||
06716330d6
|
|||
1a6346677e
|
|||
261be50463
|
|||
f93f5c8fdd
|
|||
039c55eb1e
|
|||
a54242de8a
|
|||
578e51dc17
|
|||
8e2ec4dfe2
|
|||
3431f091ad
|
|||
a988003bdd
|
|||
5561f574cf
|
|||
8fd4561721
|
|||
9fe68be3f4
|
|||
90dac674eb
|
|||
1ebf15b039
|
|||
86acef1a22
|
|||
277e241183
|
|||
3b41ea7b5f
|
|||
6ce0ef7d55
|
|||
fd09ac496e
|
|||
9ca56bd910
|
|||
3e891e19c3
|
|||
9341988017
|
|||
461e403733
|
|||
c03e2d328d
|
|||
f3723ee43b
|
|||
8f7b566db7
|
|||
70a2a01ec3
|
|||
c31155738e
|
|||
c23fac465f
|
|||
51a2ac2d66
|
|||
a5e8366f1b
|
|||
3b698dbf33
|
|||
dc2c2d7ce8
|
|||
bb5634adc4
|
|||
d26e16eadf
|
|||
8f5f6065d8
|
|||
ad299bf762
|
|||
f70bbd53d9
|
|||
ae821f738e
|
|||
ab7f5d0a41
|
|||
1bddde7ccd
|
|||
58259c0bb4
|
|||
a84a453990
|
|||
563c644f48
|
|||
f8331a571e
|
|||
1ed26a16ea
|
|||
72c13cbb58
|
|||
e420add646
|
|||
6f1149ad5e
|
|||
b9899922e0
|
|||
6a2933ea0a
|
|||
be5c76c3bd
|
|||
217c1f9e10
|
|||
725ab7accf
|
|||
9a7a2cffc2
|
|||
6d3db23f80
|
|||
4bc9626013
|
|||
8002236c1f
|
|||
c4fe6c4391
|
|||
ebfe972a98
|
|||
20aac4c47a
|
|||
35308e0995
|
|||
f7f32c3f6a
|
|||
4dc781b35c
|
|||
849ef18adb
|
|||
77aa15e557
|
|||
47b106503d
|
|||
bde4cb06f4
|
|||
d772f7ed71
|
|||
0662af9b1a
|
|||
03fd730139
|
|||
d362c96b80
|
|||
9f34c9dffa
|
|||
cacf43d1d8
|
|||
edc23d616d
|
|||
db191f87fd
|
|||
e8677c09b4
|
|||
378c54908e
|
|||
07af4ae00f
|
|||
c262e5aaa4
|
|||
96e125b812
|
|||
c0123b9f91
|
|||
2ec9c7cbc3
|
|||
4cfe19d369
|
|||
f449859ad9
|
|||
ae5630cf96
|
|||
75c8202c77
|
|||
e9e8c95a19
|
|||
879a69608c
|
|||
560c515ecf
|
|||
ec8c00853a
|
|||
5888d13d7c
|
|||
15db0cca8e
|
1
.idea/Discord-History-Tracker.iml
generated
1
.idea/Discord-History-Tracker.iml
generated
@@ -8,6 +8,7 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
|
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/bld" />
|
<excludeFolder url="file://$MODULE_DIR$/bld" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/lib" />
|
<excludeFolder url="file://$MODULE_DIR$/lib" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/app" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.8" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.8" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
52
.vscode/tasks.json
vendored
52
.vscode/tasks.json
vendored
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Build Raw",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "python ./build.py --nominify --copytracker",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "shared",
|
|
||||||
"echo": true,
|
|
||||||
"focus": false,
|
|
||||||
"showReuseMessage": false
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Build Minified",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "python ./build.py --copytracker",
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "shared",
|
|
||||||
"echo": true,
|
|
||||||
"focus": false,
|
|
||||||
"showReuseMessage": false
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Build Website",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "python ./build.py --website",
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "shared",
|
|
||||||
"echo": true,
|
|
||||||
"focus": false,
|
|
||||||
"showReuseMessage": false
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
59
README.md
59
README.md
@@ -1,48 +1,55 @@
|
|||||||
# Welcome
|
# Welcome
|
||||||
|
|
||||||
All you need to **use Discord History Tracker** is either an up-to-date browser, or the [Discord desktop client](https://discord.com/download). Visit the [official website](https://dht.chylex.com) for instructions.
|
For instructions on how to **use Discord History Tracker**, visit the [official website](https://dht.chylex.com).
|
||||||
|
|
||||||
To **report an issue or suggestion**, first please see the [issues](https://github.com/chylex/Discord-History-Tracker/issues) page and make sure someone else hasn't already created a similar issue report. If you do find an existing issue, comment on it or add a reaction. Otherwise, either click [New Issue](https://github.com/chylex/Discord-History-Tracker/issues/new), or contact me via email [contact@chylex.com](mailto:contact@chylex.com) or Twitter [@chylexmc](https://twitter.com/chylexmc).
|
To **report an issue or suggestion**, first please see the [issues](https://github.com/chylex/Discord-History-Tracker/issues) page and make sure someone else hasn't already created a similar issue report. If you do find an existing issue, comment on it or add a reaction. Otherwise, either click [New Issue](https://github.com/chylex/Discord-History-Tracker/issues/new), or contact me via email [contact@chylex.com](mailto:contact@chylex.com) or Twitter [@chylexmc](https://twitter.com/chylexmc).
|
||||||
|
|
||||||
If you are interested in **creating your own version** from the source code, continue reading the [build instructions](#Build-Instructions) below.
|
If you are interested in **building from source code**, continue reading the [build instructions](#Build-Instructions) below.
|
||||||
|
|
||||||
|
This branch is dedicated to the Discord History Tracker desktop app. If you are looking for the older browser-only version, visit the [master-browser-only](https://github.com/chylex/Discord-History-Tracker/tree/master-browser-only) branch.
|
||||||
|
|
||||||
# Build Instructions
|
# Build Instructions
|
||||||
|
|
||||||
Follow the steps below to create your own version of Discord History Tracker.
|
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
Fork the repository and clone it to your computer (if you've never used git, you can download the [GitHub Desktop](https://desktop.github.com) client to get started quickly).
|
Fork the repository and clone it to your computer (if you've never used git, you can download the [GitHub Desktop](https://desktop.github.com) client to get started quickly).
|
||||||
|
|
||||||
Now you can modify the source code:
|
Folder organization:
|
||||||
* `src/tracker/` contains JS files that are automatically combined into the **tracker bookmark/script**
|
* `app/` contains a Visual Studio solution for the desktop app
|
||||||
* `src/viewer/` contains HTML, CSS, JS files that are then combined into the **offline viewer page**
|
|
||||||
* `lib/` contains utilities required to build the project
|
* `lib/` contains utilities required to build the project
|
||||||
* `web/` contains source code of the [official website](https://dht.chylex.com), which can be used as a template when making your own website
|
* `web/` contains source code of the [official website](https://dht.chylex.com), which can be used as a template when making your own website
|
||||||
|
|
||||||
|
To start editing source code for the desktop app, install the [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0), and then open `app/DiscordHistoryTracker.sln` in [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Rider](https://www.jetbrains.com/rider/).
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
After you've done changes to the source code, you will need to build it. Before that, download and install:
|
To build a `Debug` version of the desktop app, there are no additional requirements.
|
||||||
* (**required**) [Python 3](https://www.python.org/downloads)
|
|
||||||
* Use to run the build script
|
|
||||||
* (optional) [Node + npm](https://nodejs.org/en) & command line [uglify-js](https://www.npmjs.com/package/uglify-js)
|
|
||||||
* Not required on Windows
|
|
||||||
* Only required for optional [JS minification](#Minification) on Linux/Mac
|
|
||||||
|
|
||||||
Now open the folder that contains `build.py` in a command line, and run `python build.py` to create a build with default settings. The following files will be created:
|
To build a `Release` version of the desktop app, follow the instructions for your operating system.
|
||||||
* `bld/track.js` is the raw tracker script that can be pasted into a browser console
|
|
||||||
* `bld/track.html` is the tracker script but sanitized for inclusion in HTML (see `web/index.php` for examples)
|
|
||||||
* `bld/viewer.html` is the complete offline viewer
|
|
||||||
|
|
||||||
You can tweak the build process using the following flags:
|
#### Release – Windows (64-bit)
|
||||||
* `python build.py --nominify` to disable [minification](#Minification)
|
|
||||||
|
|
||||||
### Minification
|
1. Install [Python 3](https://www.python.org/downloads), and ensure the `python` executable is in your `PATH`
|
||||||
|
2. Install [Powershell 5](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows) or newer (on Windows 10, the included version of Powershell should be enough)
|
||||||
|
|
||||||
The build process automatically minifies JS using `UglifyJS@3`, and CSS using a custom minifier.
|
The `lib/` folder contains an installation of [Node](https://nodejs.org/en) and [uglify-js](https://www.npmjs.com/package/uglify-js), which are used to minify the tracking script. This installation will only work on 64-bit Windows; building on 32-bit Windows is not supported, but you can try.
|
||||||
|
|
||||||
* If the `--nominify` flag is used, minification will be completely disabled
|
Run the `app/build.bat` script, and read the [Distribution](#distribution) section below.
|
||||||
* If `uglify-js` is not available from the command line, JS minification will be skipped
|
|
||||||
* When building on Windows 64-bit, the build script will use the included Node runner and packages
|
#### Release – Other Operating Systems
|
||||||
* When building on Windows 32-bit, you will need to download [Node 32-bit](https://nodejs.org/en/download) and replace the included one in `lib/`
|
|
||||||
* When building on Linux/Mac, the build script will attempt to find `uglifyjs` in the command line
|
1. Install [Python 3](https://www.python.org/downloads), and ensure the `python` executable exists and launches Python 3
|
||||||
|
- On Debian and derivatives, you can install `python-is-python3`
|
||||||
|
- On other distributions, you can create a link manually, for ex. `ln -s /usr/bin/python3 /usr/bin/python`
|
||||||
|
- If you don't want `python` to mean Python 3, then edit `Desktop.csproj` and change `python` to `python3`
|
||||||
|
2. Install [Node + npm](https://nodejs.org/en)
|
||||||
|
3. Install [uglify-js](https://www.npmjs.com/package/uglify-js) globally (`npm install -g uglify-js`)
|
||||||
|
4. Install the `zip` package from your repository
|
||||||
|
|
||||||
|
Run the `app/build.sh` script, and read the [Distribution](#distribution) section below.
|
||||||
|
|
||||||
|
#### Distribution
|
||||||
|
|
||||||
|
The mentioned build scripts will prepare `Release` builds ready for distribution. Once the script finishes, the `app/bin` folder will contain self-contained executables for each major operating system, and a portable version that works on all other systems but requires .NET 8 to be installed.
|
||||||
|
|
||||||
|
Note that when building on Windows, the generated `.zip` files for Linux and Mac will not have correct file permissions, so it will not be possible to run them by double-clicking `DiscordHistoryTracker`. I tried using Python to re-create the archives with correct file permissions, but found that Linux `zip` tools could not see them. The only working solution is building the Windows + portable version on Windows, and Linux + Mac version on Linux.
|
||||||
|
1
app/.idea/.idea.DiscordHistoryTracker/.idea/.gitignore
generated
vendored
1
app/.idea/.idea.DiscordHistoryTracker/.idea/.gitignore
generated
vendored
@@ -2,7 +2,6 @@
|
|||||||
/contentModel.xml
|
/contentModel.xml
|
||||||
/dataSources
|
/dataSources
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
/dataSources.xml
|
|
||||||
/dictionaries
|
/dictionaries
|
||||||
/httpRequests/
|
/httpRequests/
|
||||||
/misc.xml
|
/misc.xml
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AvaloniaProject">
|
|
||||||
<option name="projectPerEditor">
|
|
||||||
<map>
|
|
||||||
<entry key="Desktop/App.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Dialogs/MessageDialog.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/AboutWindow.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/Controls/StatusBar.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/MainContentScreen.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/MainWindow.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/Pages/DatabasePage.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/Pages/TrackingPage.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/Pages/ViewerPage.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
<entry key="Desktop/Main/WelcomeScreen.axaml" value="Desktop/Desktop.csproj" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@@ -13,10 +13,10 @@
|
|||||||
<CssCodeStyleSettings>
|
<CssCodeStyleSettings>
|
||||||
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
||||||
</CssCodeStyleSettings>
|
</CssCodeStyleSettings>
|
||||||
<DB2CodeStyleSettings version="5">
|
<DB2CodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</DB2CodeStyleSettings>
|
</DB2CodeStyleSettings>
|
||||||
<DerbyCodeStyleSettings version="5">
|
<DerbyCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</DerbyCodeStyleSettings>
|
</DerbyCodeStyleSettings>
|
||||||
<GoCodeStyleSettings>
|
<GoCodeStyleSettings>
|
||||||
@@ -26,10 +26,13 @@
|
|||||||
<option name="WRAP_FUNC_PARAMS" value="5" />
|
<option name="WRAP_FUNC_PARAMS" value="5" />
|
||||||
<option name="WRAP_FUNC_RESULT" value="5" />
|
<option name="WRAP_FUNC_RESULT" value="5" />
|
||||||
</GoCodeStyleSettings>
|
</GoCodeStyleSettings>
|
||||||
<H2CodeStyleSettings version="5">
|
<H2CodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</H2CodeStyleSettings>
|
</H2CodeStyleSettings>
|
||||||
<HSQLCodeStyleSettings version="5">
|
<H2CodeStyleSettings version="6">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</H2CodeStyleSettings>
|
||||||
|
<HSQLCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</HSQLCodeStyleSettings>
|
</HSQLCodeStyleSettings>
|
||||||
<HTMLCodeStyleSettings>
|
<HTMLCodeStyleSettings>
|
||||||
@@ -85,10 +88,10 @@
|
|||||||
<LessCodeStyleSettings>
|
<LessCodeStyleSettings>
|
||||||
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
||||||
</LessCodeStyleSettings>
|
</LessCodeStyleSettings>
|
||||||
<MSSQLCodeStyleSettings version="5">
|
<MSSQLCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</MSSQLCodeStyleSettings>
|
</MSSQLCodeStyleSettings>
|
||||||
<MySQLCodeStyleSettings version="5">
|
<MySQLCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</MySQLCodeStyleSettings>
|
</MySQLCodeStyleSettings>
|
||||||
<Objective-C>
|
<Objective-C>
|
||||||
@@ -98,7 +101,7 @@
|
|||||||
<option name="SPACE_BEFORE_INIT_LIST" value="true" />
|
<option name="SPACE_BEFORE_INIT_LIST" value="true" />
|
||||||
<option name="SPACE_AFTER_DICTIONARY_LITERAL_COLON" value="false" />
|
<option name="SPACE_AFTER_DICTIONARY_LITERAL_COLON" value="false" />
|
||||||
</Objective-C>
|
</Objective-C>
|
||||||
<OracleCodeStyleSettings version="5">
|
<OracleCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</OracleCodeStyleSettings>
|
</OracleCodeStyleSettings>
|
||||||
<PHPCodeStyleSettings>
|
<PHPCodeStyleSettings>
|
||||||
@@ -116,7 +119,7 @@
|
|||||||
<option name="NEW_LINE_AFTER_PHP_OPENING_TAG" value="true" />
|
<option name="NEW_LINE_AFTER_PHP_OPENING_TAG" value="true" />
|
||||||
<option name="SPACE_AROUND_ASSIGNMENT_IN_DECLARE" value="true" />
|
<option name="SPACE_AROUND_ASSIGNMENT_IN_DECLARE" value="true" />
|
||||||
</PHPCodeStyleSettings>
|
</PHPCodeStyleSettings>
|
||||||
<PostgresCodeStyleSettings version="5">
|
<PostgresCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</PostgresCodeStyleSettings>
|
</PostgresCodeStyleSettings>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -143,13 +146,13 @@
|
|||||||
<option name="INDENT_WHEN_CASES" value="true" />
|
<option name="INDENT_WHEN_CASES" value="true" />
|
||||||
<option name="CHAIN_CALLS_ALIGNMENT" value="2" />
|
<option name="CHAIN_CALLS_ALIGNMENT" value="2" />
|
||||||
</Ruby>
|
</Ruby>
|
||||||
<SQLiteCodeStyleSettings version="5">
|
<SQLiteCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</SQLiteCodeStyleSettings>
|
</SQLiteCodeStyleSettings>
|
||||||
<ScssCodeStyleSettings>
|
<ScssCodeStyleSettings>
|
||||||
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
||||||
</ScssCodeStyleSettings>
|
</ScssCodeStyleSettings>
|
||||||
<SqlCodeStyleSettings version="5">
|
<SqlCodeStyleSettings version="6">
|
||||||
<option name="KEYWORD_CASE" value="2" />
|
<option name="KEYWORD_CASE" value="2" />
|
||||||
<option name="TYPE_CASE" value="2" />
|
<option name="TYPE_CASE" value="2" />
|
||||||
<option name="CUSTOM_TYPE_CASE" value="2" />
|
<option name="CUSTOM_TYPE_CASE" value="2" />
|
||||||
@@ -175,7 +178,7 @@
|
|||||||
<option name="VIEW_INDENT_QUERY" value="true" />
|
<option name="VIEW_INDENT_QUERY" value="true" />
|
||||||
<option name="EXPR_CASE_WHEN_WRAP" value="false" />
|
<option name="EXPR_CASE_WHEN_WRAP" value="false" />
|
||||||
</SqlCodeStyleSettings>
|
</SqlCodeStyleSettings>
|
||||||
<SybaseCodeStyleSettings version="5">
|
<SybaseCodeStyleSettings version="6">
|
||||||
<option name="USE_GENERIC_STYLE" value="true" />
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
</SybaseCodeStyleSettings>
|
</SybaseCodeStyleSettings>
|
||||||
<TypeScriptCodeStyleSettings version="0">
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
12
app/.idea/.idea.DiscordHistoryTracker/.idea/dataSources.xml
generated
Normal file
12
app/.idea/.idea.DiscordHistoryTracker/.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="empty.dht" read-only="true" uuid="e01b8deb-489e-470a-9011-f6548927191e">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/empty.dht</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
@@ -1,6 +1,6 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Desktop" type="DotNetProject" factoryName=".NET Project">
|
<configuration default="false" name="Desktop" type="DotNetProject" factoryName=".NET Project">
|
||||||
<option name="EXE_PATH" value="$PROJECT_DIR$/Desktop/bin/Debug/net5.0/Desktop.exe" />
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Desktop/bin/Debug/net5.0/DiscordHistoryTracker.exe" />
|
||||||
<option name="PROGRAM_PARAMETERS" value="" />
|
<option name="PROGRAM_PARAMETERS" value="" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Desktop/bin/Debug/net5.0" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Desktop/bin/Debug/net5.0" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectTasksOptions">
|
|
||||||
<enabled-global>
|
|
||||||
<option value="UglifyJS (Tracker)" />
|
|
||||||
</enabled-global>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@@ -1,73 +0,0 @@
|
|||||||
<Application xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
x:Class="DHT.Desktop.App">
|
|
||||||
|
|
||||||
<Application.Styles>
|
|
||||||
|
|
||||||
<FluentTheme Mode="Light" />
|
|
||||||
|
|
||||||
<Style Selector="Button">
|
|
||||||
<Setter Property="Padding" Value="10 7 10 6" />
|
|
||||||
<Setter Property="Background" Value="#D2D2D2" />
|
|
||||||
<Setter Property="BorderBrush" Value="#999999" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="TextBlock.Foreground" Value="#070707" />
|
|
||||||
<Setter Property="Background" Value="#D2D2D2" />
|
|
||||||
<Setter Property="BorderBrush" Value="#999999" />
|
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="#DEDEDE" />
|
|
||||||
<Setter Property="BorderBrush" Value="#A2A2A2" />
|
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button:pressed">
|
|
||||||
<Setter Property="RenderTransform" Value="none" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="#E5E5E5" />
|
|
||||||
<Setter Property="BorderBrush" Value="#A5A5A5" />
|
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="TextBlock.Foreground" Value="#909090" />
|
|
||||||
<Setter Property="Background" Value="#E9E9E9" />
|
|
||||||
<Setter Property="BorderBrush" Value="#BFBFBF" />
|
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="TextBox">
|
|
||||||
<Setter Property="BorderBrush" Value="#999999" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="SelectionBrush" Value="#72C0FF" />
|
|
||||||
<Setter Property="Background" Value="#F4F4F4" />
|
|
||||||
<Setter Property="Padding" Value="6 0" />
|
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="TextBox /template/ Border#PART_BorderElement">
|
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
|
|
||||||
<Setter Property="BorderBrush" Value="#999999" />
|
|
||||||
<Setter Property="Background" Value="#F8F8F8" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
|
||||||
<Setter Property="BorderBrush" Value="#546A9F" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="Background" Value="#FBFBFB" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="TextBox:disabled /template/ Border#PART_BorderElement">
|
|
||||||
<Setter Property="BorderBrush" Value="#999999" />
|
|
||||||
<Setter Property="Background" Value="#BBBBBB" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="TextBox /template/ TextBlock#PART_Watermark">
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
</Application.Styles>
|
|
||||||
|
|
||||||
</Application>
|
|
@@ -1,20 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using DHT.Desktop.Main;
|
|
||||||
|
|
||||||
namespace DHT.Desktop {
|
|
||||||
public class App : Application {
|
|
||||||
public override void Initialize() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted() {
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
|
||||||
desktop.MainWindow = new MainWindow(new Arguments(desktop.Args));
|
|
||||||
}
|
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
135
app/Desktop/App/App.axaml
Normal file
135
app/Desktop/App/App.axaml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||||
|
xmlns:common="clr-namespace:DHT.Desktop.App.Common"
|
||||||
|
x:Class="DHT.Desktop.App.App">
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
|
||||||
|
<FluentTheme />
|
||||||
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" />
|
||||||
|
|
||||||
|
<Style Selector="Button, CheckBox, RadioButton, Expander /template/ ToggleButton#ExpanderHeader">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="Padding" Value="10 7 10 6" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button:pressed">
|
||||||
|
<Setter Property="RenderTransform" Value="none" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="Padding" Value="6 0" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox /template/ TextBlock#PART_Watermark">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox:error DataValidationErrors">
|
||||||
|
<Style.Resources>
|
||||||
|
<ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors">
|
||||||
|
<ContentPresenter Name="PART_ContentPresenter"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{TemplateBinding CornerRadius}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Style.Resources>
|
||||||
|
<Setter Property="Template" Value="{StaticResource InlineDataValidationContentTemplate}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Expander">
|
||||||
|
<Setter Property="MinHeight" Value="40" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</Application.Styles>
|
||||||
|
|
||||||
|
<Application.Resources>
|
||||||
|
|
||||||
|
<common:NumberValueConverter x:Key="NumberValueConverter" />
|
||||||
|
<common:BytesValueConverter x:Key="BytesValueConverter" />
|
||||||
|
|
||||||
|
<system:Double x:Key="ControlContentThemeFontSize">14</system:Double>
|
||||||
|
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
|
||||||
|
|
||||||
|
<Color x:Key="SystemAccentColor">#3C5F95</Color>
|
||||||
|
<Color x:Key="SystemAccentColorDark1">#3C5F95</Color>
|
||||||
|
<Color x:Key="SystemAccentColorDark2">#3C5F95</Color>
|
||||||
|
<Color x:Key="SystemAccentColorDark3">#3C5F95</Color>
|
||||||
|
<Color x:Key="SystemAccentColorLight1">#3C5F95</Color>
|
||||||
|
<Color x:Key="SystemAccentColorLight2">#3C5F95</Color>
|
||||||
|
<Color x:Key="SystemAccentColorLight3">#3C5F95</Color>
|
||||||
|
|
||||||
|
<system:Double x:Key="ScrollBarSize">14</system:Double>
|
||||||
|
<TransformOperations x:Key="VerticalSmallScrollThumbScaleTransform">scaleX(0.5) translateX(-3px)</TransformOperations>
|
||||||
|
<TransformOperations x:Key="HorizontalSmallScrollThumbScaleTransform">scaleY(0.5) translateY(-3px)</TransformOperations>
|
||||||
|
<SolidColorBrush x:Key="ScrollBarPanningThumbBackground" Color="#8F8F8F" />
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="ButtonBorderBrush" Color="#3C5F95" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBorderBrushPointerOver" Color="#0E2B59" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBorderBrushPressed" Color="#061742" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBorderBrushDisabled" Color="#9B9B9B" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBackground" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#F6F9FD" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBackgroundPressed" Color="#EDF3FD" />
|
||||||
|
<SolidColorBrush x:Key="ButtonBackgroundDisabled" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ButtonForeground" Color="#112961" />
|
||||||
|
<SolidColorBrush x:Key="ButtonForegroundPointerOver" Color="#050E41" />
|
||||||
|
<SolidColorBrush x:Key="ButtonForegroundPressed" Color="#010320" />
|
||||||
|
<SolidColorBrush x:Key="ButtonForegroundDisabled" Color="#8C8C8C" />
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#515151" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#000000" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#3C5F95" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBorderBrushDisabled" Color="#9B9B9B" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBackground" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#F8FCFF" />
|
||||||
|
<SolidColorBrush x:Key="TextControlBackgroundDisabled" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="TextControlForeground" Color="#000000" />
|
||||||
|
<SolidColorBrush x:Key="TextControlForegroundPointerOver" Color="#000000" />
|
||||||
|
<SolidColorBrush x:Key="TextControlForegroundFocused" Color="#000000" />
|
||||||
|
<SolidColorBrush x:Key="TextControlForegroundDisabled" Color="#8C8C8C" />
|
||||||
|
<SolidColorBrush x:Key="TextControlSelectionHighlightColor" Color="#DEE9F8" />
|
||||||
|
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#AAAAAA" />
|
||||||
|
<SolidColorBrush x:Key="TextControlPlaceholderForegroundPointerOver" Color="#AAAAAA" />
|
||||||
|
<SolidColorBrush x:Key="TextControlPlaceholderForegroundFocused" Color="#AAAAAA" />
|
||||||
|
<SolidColorBrush x:Key="TextControlPlaceholderForegroundDisabled" Color="#AAAAAA" />
|
||||||
|
|
||||||
|
<Thickness x:Key="ExpanderHeaderPadding">15,0</Thickness>
|
||||||
|
<Thickness x:Key="ExpanderContentPadding">12</Thickness>
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBorderBrush" Color="#697DAB" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPointerOver" Color="#697DAB" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushPressed" Color="#697DAB" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBorderBrushDisabled" Color="#697DAB" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="#697DAB" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPointerOver" Color="#536794" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPressed" Color="#47587F" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderBackgroundDisabled" Color="#697DAB" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderForeground" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderForegroundPointerOver" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderForegroundPressed" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderHeaderForegroundDisabled" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronBackground" Color="Transparent" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronBackgroundPointerOver" Color="#536794" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronBackgroundPressed" Color="#47587F" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronBackgroundDisabled" Color="Transparent" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronForeground" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronForegroundPointerOver" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronForegroundPressed" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ExpanderChevronForegroundDisabled" Color="#FFFFFF" />
|
||||||
|
|
||||||
|
</Application.Resources>
|
||||||
|
|
||||||
|
</Application>
|
21
app/Desktop/App/App.axaml.cs
Normal file
21
app/Desktop/App/App.axaml.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using DHT.Desktop.App.Windows;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App;
|
||||||
|
|
||||||
|
sealed class App : Application {
|
||||||
|
public override void Initialize() {
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted() {
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||||
|
desktop.MainWindow = new MainWindow(new Arguments(desktop.Args ?? Array.Empty<string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
53
app/Desktop/App/Common/BytesValueConverter.cs
Normal file
53
app/Desktop/App/Common/BytesValueConverter.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Common;
|
||||||
|
|
||||||
|
sealed class BytesValueConverter : IValueConverter {
|
||||||
|
private sealed class Unit {
|
||||||
|
private readonly string label;
|
||||||
|
private readonly string numberFormat;
|
||||||
|
|
||||||
|
public Unit(string label, int decimalPlaces) {
|
||||||
|
this.label = label;
|
||||||
|
this.numberFormat = "{0:n" + decimalPlaces + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Format(double size) {
|
||||||
|
return string.Format(Program.Culture, numberFormat, size) + " " + label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Unit[] Units = {
|
||||||
|
new ("B", decimalPlaces: 0),
|
||||||
|
new ("kB", decimalPlaces: 0),
|
||||||
|
new ("MB", decimalPlaces: 1),
|
||||||
|
new ("GB", decimalPlaces: 1),
|
||||||
|
new ("TB", decimalPlaces: 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
private const int Scale = 1000;
|
||||||
|
|
||||||
|
private static string Convert(ulong size) {
|
||||||
|
int power = size == 0L ? 0 : (int) Math.Log(size, Scale);
|
||||||
|
int unit = power >= Units.Length ? Units.Length - 1 : power;
|
||||||
|
return Units[unit].Format(unit == 0 ? size : size / Math.Pow(Scale, unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||||
|
if (value is long size and >= 0L) {
|
||||||
|
return Convert((ulong) size);
|
||||||
|
}
|
||||||
|
else if (value is ulong usize) {
|
||||||
|
return Convert(usize);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
68
app/Desktop/App/Common/DatabaseGui.cs
Normal file
68
app/Desktop/App/Common/DatabaseGui.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using DHT.Desktop.App.Dialogs.File;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Database.Exceptions;
|
||||||
|
using DHT.Server.Database.Sqlite;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Common;
|
||||||
|
|
||||||
|
static class DatabaseGui {
|
||||||
|
private static readonly Log Log = Log.ForType(typeof(DatabaseGui));
|
||||||
|
|
||||||
|
private const string DatabaseFileInitialName = "archive.dht";
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<FilePickerFileType> DatabaseFileDialogFilter = new List<FilePickerFileType> {
|
||||||
|
FileDialogs.CreateFilter("Discord History Tracker Database", new [] { "dht" })
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<string[]> NewOpenDatabaseFilesDialog(Window window, string? suggestedDirectory) {
|
||||||
|
return await window.StorageProvider.OpenFiles(new FilePickerOpenOptions {
|
||||||
|
Title = "Open Database File",
|
||||||
|
FileTypeFilter = DatabaseFileDialogFilter,
|
||||||
|
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory),
|
||||||
|
AllowMultiple = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string?> NewOpenOrCreateDatabaseFileDialog(Window window, string? suggestedDirectory) {
|
||||||
|
return await window.StorageProvider.SaveFile(new FilePickerSaveOptions {
|
||||||
|
Title = "Open or Create Database File",
|
||||||
|
FileTypeChoices = DatabaseFileDialogFilter,
|
||||||
|
SuggestedFileName = DatabaseFileInitialName,
|
||||||
|
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, suggestedDirectory),
|
||||||
|
ShowOverwritePrompt = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IDatabaseFile?> TryOpenOrCreateDatabaseFromPath(string path, Window window, Func<Task<bool>> checkCanUpgradeDatabase) {
|
||||||
|
IDatabaseFile? file = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
file = await SqliteDatabaseFile.OpenOrCreate(path, checkCanUpgradeDatabase);
|
||||||
|
} catch (InvalidDatabaseVersionException ex) {
|
||||||
|
await Dialog.ShowOk(window, "Database Error", "Database '" + Path.GetFileName(path) + "' appears to be corrupted (invalid version: " + ex.Version + ").");
|
||||||
|
} catch (DatabaseTooNewException ex) {
|
||||||
|
await Dialog.ShowOk(window, "Database Error", "Database '" + Path.GetFileName(path) + "' was opened in a newer version of DHT (database version " + ex.DatabaseVersion + ", app version " + ex.CurrentVersion + ").");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error(ex);
|
||||||
|
await Dialog.ShowOk(window, "Database Error", "Database '" + Path.GetFileName(path) + "' could not be opened:" + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DialogResult.YesNo> ShowCanUpgradeDatabaseDialog(Window window) {
|
||||||
|
return await Dialog.ShowYesNo(window, "Database Upgrade", "This database was created with an older version of DHT. If you proceed, the database will be upgraded and will no longer open in previous versions of DHT.\n\nPlease ensure you have a backup of the database. Do you want to proceed with the upgrade?");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DialogResult.YesNo> ShowCanUpgradeMultipleDatabaseDialog(Window window) {
|
||||||
|
return await Dialog.ShowYesNo(window, "Database Upgrade", "One or more databases were created with an older version of DHT. If you proceed, these databases will be upgraded and will no longer open in previous versions of DHT. Otherwise, these databases will be skipped.\n\nPlease ensure you have a backup of the databases. Do you want to proceed with the upgrade?");
|
||||||
|
}
|
||||||
|
}
|
15
app/Desktop/App/Common/NumberValueConverter.cs
Normal file
15
app/Desktop/App/Common/NumberValueConverter.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Common;
|
||||||
|
|
||||||
|
sealed class NumberValueConverter : IValueConverter {
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||||
|
return value == null ? "-" : string.Format(Program.Culture, "{0:n0}", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
19
app/Desktop/App/Common/TextFormat.cs
Normal file
19
app/Desktop/App/Common/TextFormat.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace DHT.Desktop.App.Common;
|
||||||
|
|
||||||
|
static class TextFormat {
|
||||||
|
public static string Format(this int number) {
|
||||||
|
return number.ToString("N0", Program.Culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Format(this long number) {
|
||||||
|
return number.ToString("N0", Program.Culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Pluralize(this int number, string singular) {
|
||||||
|
return number.Format() + "\u00A0" + (number == 1 ? singular : singular + "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Pluralize(this long number, string singular) {
|
||||||
|
return number.Format() + "\u00A0" + (number == 1 ? singular : singular + "s");
|
||||||
|
}
|
||||||
|
}
|
48
app/Desktop/App/Controls/ServerConfigurationPanel.axaml
Normal file
48
app/Desktop/App/Controls/ServerConfigurationPanel.axaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:controls="clr-namespace:DHT.Desktop.App.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Class="DHT.Desktop.App.Controls.ServerConfigurationPanel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<controls:ServerConfigurationPanelModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="FontFamily" Value="Consolas,Courier" />
|
||||||
|
<Setter Property="FontSize" Value="15" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="WrapPanel > StackPanel">
|
||||||
|
<Setter Property="Orientation" Value="Vertical" />
|
||||||
|
<Setter Property="Margin" Value="0 0 10 10" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<Button Command="{Binding OnClickToggleServerButton}" Content="{Binding ToggleServerButtonText}" IsEnabled="{Binding IsToggleServerButtonEnabled}" />
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0 15">
|
||||||
|
The following settings determine how the tracking script communicates with this application. If you change them, you will have to copy/paste the tracking script again.
|
||||||
|
</TextBlock>
|
||||||
|
<WrapPanel>
|
||||||
|
<StackPanel>
|
||||||
|
<Label Target="Port">Port</Label>
|
||||||
|
<TextBox x:Name="Port" Width="70" Text="{Binding InputPort}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel>
|
||||||
|
<Label Target="Token">Token</Label>
|
||||||
|
<TextBox x:Name="Token" Width="200" Text="{Binding InputToken}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel VerticalAlignment="Bottom">
|
||||||
|
<Button Command="{Binding OnClickRandomizeToken}">Randomize Token</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</WrapPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickApplyChanges}">Apply & Restart</Button>
|
||||||
|
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickCancelChanges}">Cancel</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
11
app/Desktop/App/Controls/ServerConfigurationPanel.axaml.cs
Normal file
11
app/Desktop/App/Controls/ServerConfigurationPanel.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Controls;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class ServerConfigurationPanel : UserControl {
|
||||||
|
public ServerConfigurationPanel() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
116
app/Desktop/App/Controls/ServerConfigurationPanelModel.cs
Normal file
116
app/Desktop/App/Controls/ServerConfigurationPanelModel.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Controls;
|
||||||
|
|
||||||
|
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
||||||
|
private string inputPort;
|
||||||
|
|
||||||
|
public string InputPort {
|
||||||
|
get => inputPort;
|
||||||
|
set {
|
||||||
|
Change(ref inputPort, value);
|
||||||
|
OnPropertyChanged(nameof(HasMadeChanges));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string inputToken;
|
||||||
|
|
||||||
|
public string InputToken {
|
||||||
|
get => inputToken;
|
||||||
|
set {
|
||||||
|
Change(ref inputToken, value);
|
||||||
|
OnPropertyChanged(nameof(HasMadeChanges));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasMadeChanges => ServerManager.Port.ToString() != InputPort || ServerManager.Token != InputToken;
|
||||||
|
|
||||||
|
private bool isToggleServerButtonEnabled = true;
|
||||||
|
|
||||||
|
public bool IsToggleServerButtonEnabled {
|
||||||
|
get => isToggleServerButtonEnabled;
|
||||||
|
set => Change(ref isToggleServerButtonEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToggleServerButtonText => serverManager.IsRunning ? "Stop Server" : "Start Server";
|
||||||
|
|
||||||
|
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly ServerManager serverManager;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public ServerConfigurationPanelModel() : this(null!, new ServerManager(DummyDatabaseFile.Instance)) {}
|
||||||
|
|
||||||
|
public ServerConfigurationPanelModel(Window window, ServerManager serverManager) {
|
||||||
|
this.window = window;
|
||||||
|
this.serverManager = serverManager;
|
||||||
|
this.inputPort = ServerManager.Port.ToString();
|
||||||
|
this.inputToken = ServerManager.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize() {
|
||||||
|
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
|
||||||
|
ServerStatusChanged?.Invoke(this, serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
|
||||||
|
OnPropertyChanged(nameof(ToggleServerButtonText));
|
||||||
|
IsToggleServerButtonEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeforeServerStart() {
|
||||||
|
IsToggleServerButtonEnabled = false;
|
||||||
|
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Starting);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartServer() {
|
||||||
|
BeforeServerStart();
|
||||||
|
serverManager.Launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopServer() {
|
||||||
|
IsToggleServerButtonEnabled = false;
|
||||||
|
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Stopping);
|
||||||
|
serverManager.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClickToggleServerButton() {
|
||||||
|
if (serverManager.IsRunning) {
|
||||||
|
StopServer();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StartServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClickRandomizeToken() {
|
||||||
|
InputToken = ServerUtils.GenerateRandomToken(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OnClickApplyChanges() {
|
||||||
|
if (!ushort.TryParse(InputPort, out ushort port)) {
|
||||||
|
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeServerStart();
|
||||||
|
serverManager.Relaunch(port, InputToken);
|
||||||
|
OnPropertyChanged(nameof(HasMadeChanges));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClickCancelChanges() {
|
||||||
|
InputPort = ServerManager.Port.ToString();
|
||||||
|
InputToken = ServerManager.Token;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,56 +2,59 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
xmlns:controls="clr-namespace:DHT.Desktop.App.Controls"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="DHT.Desktop.Main.Controls.StatusBar">
|
x:Class="DHT.Desktop.App.Controls.StatusBar">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<controls:StatusBarModel />
|
<controls:StatusBarModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<UserControl.Background>
|
<UserControl.Background>
|
||||||
<SolidColorBrush>#546A9F</SolidColorBrush>
|
<SolidColorBrush>#3C4F79</SolidColorBrush>
|
||||||
</UserControl.Background>
|
</UserControl.Background>
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="StackPanel > TextBlock">
|
<Style Selector="StackPanel > TextBlock">
|
||||||
<Setter Property="Foreground" Value="#E0E0E0" />
|
<Setter Property="Foreground" Value="#E7E7E7" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="StackPanel > TextBlock.label">
|
<Style Selector="StackPanel > TextBlock.label">
|
||||||
<Setter Property="FontSize" Value="15" />
|
<Setter Property="FontSize" Value="15" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiLight" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="StackPanel > TextBlock.value">
|
<Style Selector="StackPanel > TextBlock.value">
|
||||||
<Setter Property="FontSize" Value="16" />
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiLight" />
|
||||||
<Setter Property="TextAlignment" Value="Right" />
|
<Setter Property="TextAlignment" Value="Right" />
|
||||||
|
<Setter Property="Margin" Value="0 1" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="StackPanel > Rectangle">
|
<Style Selector="StackPanel > Rectangle">
|
||||||
<Setter Property="Margin" Value="14 0" />
|
<Setter Property="Margin" Value="14 1" />
|
||||||
<Setter Property="Stroke" Value="#3B5287" />
|
<Setter Property="Stroke" Value="#697899" />
|
||||||
<Setter Property="StrokeThickness" Value="2" />
|
<Setter Property="StrokeThickness" Value="2" />
|
||||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="4 3">
|
<StackPanel Orientation="Horizontal" Margin="6 3">
|
||||||
<StackPanel Orientation="Vertical" Width="65">
|
<StackPanel Orientation="Vertical" Width="65">
|
||||||
<TextBlock Classes="label">Status</TextBlock>
|
<TextBlock Classes="label">Status</TextBlock>
|
||||||
<TextBlock FontSize="12" Margin="0 2 0 0" Text="{Binding StatusText}" />
|
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding StatusText}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Servers</TextBlock>
|
<TextBlock Classes="label">Servers</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, StringFormat={}{0:n0}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Channels</TextBlock>
|
<TextBlock Classes="label">Channels</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, StringFormat={}{0:n0}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<TextBlock Classes="label">Messages</TextBlock>
|
<TextBlock Classes="label">Messages</TextBlock>
|
||||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, StringFormat={}{0:n0}}" />
|
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Converter={StaticResource NumberValueConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
11
app/Desktop/App/Controls/StatusBar.axaml.cs
Normal file
11
app/Desktop/App/Controls/StatusBar.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Controls;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class StatusBar : UserControl {
|
||||||
|
public StatusBar() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
34
app/Desktop/App/Controls/StatusBarModel.cs
Normal file
34
app/Desktop/App/Controls/StatusBarModel.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Controls;
|
||||||
|
|
||||||
|
sealed class StatusBarModel : BaseModel {
|
||||||
|
private Status status = Status.Stopped;
|
||||||
|
|
||||||
|
public Status CurrentStatus {
|
||||||
|
get => status;
|
||||||
|
set {
|
||||||
|
status = value;
|
||||||
|
OnPropertyChanged(nameof(StatusText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StatusText {
|
||||||
|
get {
|
||||||
|
return CurrentStatus switch {
|
||||||
|
Status.Starting => "STARTING",
|
||||||
|
Status.Ready => "READY",
|
||||||
|
Status.Stopping => "STOPPING",
|
||||||
|
Status.Stopped => "STOPPED",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
Starting,
|
||||||
|
Ready,
|
||||||
|
Stopping,
|
||||||
|
Stopped
|
||||||
|
}
|
||||||
|
}
|
61
app/Desktop/App/Dialogs/CheckBox/CheckBoxDialog.axaml
Normal file
61
app/Desktop/App/Dialogs/CheckBox/CheckBoxDialog.axaml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:checkBox="clr-namespace:DHT.Desktop.App.Dialogs.CheckBox"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.App.Dialogs.CheckBox.CheckBoxDialog"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<checkBox:CheckBoxDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Panel.buttons">
|
||||||
|
<Setter Property="Margin" Value="0 20 0 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons > WrapPanel.right">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons Button">
|
||||||
|
<Setter Property="MinWidth" Value="80" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons > WrapPanel.left Button">
|
||||||
|
<Setter Property="Margin" Value="0 0 8 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons > WrapPanel.right Button">
|
||||||
|
<Setter Property="Margin" Value="8 0 0 0" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<ScrollViewer MaxHeight="400">
|
||||||
|
<ItemsRepeater ItemsSource="{Binding Items}">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<CheckBox IsChecked="{Binding Checked}">
|
||||||
|
<Label>
|
||||||
|
<TextBlock Text="{Binding Title}" TextWrapping="Wrap" />
|
||||||
|
</Label>
|
||||||
|
</CheckBox>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
</ScrollViewer>
|
||||||
|
<Panel Classes="buttons">
|
||||||
|
<WrapPanel Classes="left">
|
||||||
|
<Button Command="{Binding SelectAll}" IsEnabled="{Binding !AreAllSelected}">Select All</Button>
|
||||||
|
<Button Command="{Binding SelectNone}" IsEnabled="{Binding !AreNoneSelected}">Select None</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
<WrapPanel Classes="right">
|
||||||
|
<Button Click="ClickOk">OK</Button>
|
||||||
|
<Button Click="ClickCancel">Cancel</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Window>
|
21
app/Desktop/App/Dialogs/CheckBox/CheckBoxDialog.axaml.cs
Normal file
21
app/Desktop/App/Dialogs/CheckBox/CheckBoxDialog.axaml.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class CheckBoxDialog : Window {
|
||||||
|
public CheckBoxDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.OkCancel.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickCancel(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.OkCancel.Cancel);
|
||||||
|
}
|
||||||
|
}
|
70
app/Desktop/App/Dialogs/CheckBox/CheckBoxDialogModel.cs
Normal file
70
app/Desktop/App/Dialogs/CheckBox/CheckBoxDialogModel.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
class CheckBoxDialogModel : BaseModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
|
||||||
|
private IReadOnlyList<CheckBoxItem> items = Array.Empty<CheckBoxItem>();
|
||||||
|
|
||||||
|
public IReadOnlyList<CheckBoxItem> Items {
|
||||||
|
get => items;
|
||||||
|
|
||||||
|
protected set {
|
||||||
|
foreach (var item in items) {
|
||||||
|
item.PropertyChanged -= OnItemPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = value;
|
||||||
|
|
||||||
|
foreach (var item in items) {
|
||||||
|
item.PropertyChanged += OnItemPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool pauseCheckEvents = false;
|
||||||
|
|
||||||
|
public bool AreAllSelected => Items.All(static item => item.Checked);
|
||||||
|
public bool AreNoneSelected => Items.All(static item => !item.Checked);
|
||||||
|
|
||||||
|
public void SelectAll() => SetAllChecked(true);
|
||||||
|
public void SelectNone() => SetAllChecked(false);
|
||||||
|
|
||||||
|
private void SetAllChecked(bool isChecked) {
|
||||||
|
pauseCheckEvents = true;
|
||||||
|
|
||||||
|
foreach (var item in Items) {
|
||||||
|
item.Checked = isChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseCheckEvents = false;
|
||||||
|
UpdateBulkButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBulkButtons() {
|
||||||
|
OnPropertyChanged(nameof(AreAllSelected));
|
||||||
|
OnPropertyChanged(nameof(AreNoneSelected));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (!pauseCheckEvents && e.PropertyName == nameof(CheckBoxItem.Checked)) {
|
||||||
|
UpdateBulkButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class CheckBoxDialogModel<T> : CheckBoxDialogModel {
|
||||||
|
public new IReadOnlyList<CheckBoxItem<T>> Items { get; }
|
||||||
|
|
||||||
|
public IEnumerable<CheckBoxItem<T>> SelectedItems => Items.Where(static item => item.Checked);
|
||||||
|
|
||||||
|
public CheckBoxDialogModel(IEnumerable<CheckBoxItem<T>> items) {
|
||||||
|
this.Items = new List<CheckBoxItem<T>>(items);
|
||||||
|
base.Items = this.Items;
|
||||||
|
}
|
||||||
|
}
|
24
app/Desktop/App/Dialogs/CheckBox/CheckBoxItem.cs
Normal file
24
app/Desktop/App/Dialogs/CheckBox/CheckBoxItem.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
class CheckBoxItem : BaseModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
public object? Item { get; init; } = null;
|
||||||
|
|
||||||
|
private bool isChecked = false;
|
||||||
|
|
||||||
|
public bool Checked {
|
||||||
|
get => isChecked;
|
||||||
|
set => Change(ref isChecked, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class CheckBoxItem<T> : CheckBoxItem {
|
||||||
|
public new T Item { get; }
|
||||||
|
|
||||||
|
public CheckBoxItem(T item) {
|
||||||
|
this.Item = item;
|
||||||
|
base.Item = item;
|
||||||
|
}
|
||||||
|
}
|
36
app/Desktop/App/Dialogs/File/FileDialogs.cs
Normal file
36
app/Desktop/App/Dialogs/File/FileDialogs.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.File;
|
||||||
|
|
||||||
|
static class FileDialogs {
|
||||||
|
public static async Task<string[]> OpenFiles(this IStorageProvider storageProvider, FilePickerOpenOptions options) {
|
||||||
|
return (await storageProvider.OpenFilePickerAsync(options)).ToLocalPaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string?> SaveFile(this IStorageProvider storageProvider, FilePickerSaveOptions options) {
|
||||||
|
return (await storageProvider.SaveFilePickerAsync(options))?.ToLocalPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FilePickerFileType CreateFilter(string name, string[] extensions) {
|
||||||
|
return new FilePickerFileType(name) {
|
||||||
|
Patterns = extensions.Select(static ext => "*." + ext).ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<IStorageFolder?> GetSuggestedStartLocation(Window window, string? suggestedDirectory) {
|
||||||
|
return suggestedDirectory == null ? Task.FromResult<IStorageFolder?>(null) : window.StorageProvider.TryGetFolderFromPathAsync(suggestedDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToLocalPath(this IStorageFile file) {
|
||||||
|
return file.TryGetLocalPath() ?? throw new NotSupportedException("Local filesystem is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] ToLocalPaths(this IReadOnlyList<IStorageFile> files) {
|
||||||
|
return files.Select(ToLocalPath).ToArray();
|
||||||
|
}
|
||||||
|
}
|
74
app/Desktop/App/Dialogs/Message/Dialog.cs
Normal file
74
app/Desktop/App/Dialogs/Message/Dialog.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.Message;
|
||||||
|
|
||||||
|
static class Dialog {
|
||||||
|
public static async Task ShowOk(Window owner, string title, string message) {
|
||||||
|
if (!Dispatcher.UIThread.CheckAccess()) {
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => ShowOk(owner, title, message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new MessageDialog {
|
||||||
|
DataContext = new MessageDialogModel {
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
IsOkVisible = true
|
||||||
|
}
|
||||||
|
}.ShowDialog<DialogResult.All>(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DialogResult.OkCancel> ShowOkCancel(Window owner, string title, string message) {
|
||||||
|
if (!Dispatcher.UIThread.CheckAccess()) {
|
||||||
|
return await Dispatcher.UIThread.InvokeAsync(() => ShowOkCancel(owner, title, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await new MessageDialog {
|
||||||
|
DataContext = new MessageDialogModel {
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
IsOkVisible = true,
|
||||||
|
IsCancelVisible = true
|
||||||
|
}
|
||||||
|
}.ShowDialog<DialogResult.All?>(owner);
|
||||||
|
|
||||||
|
return result.ToOkCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DialogResult.YesNo> ShowYesNo(Window owner, string title, string message) {
|
||||||
|
if (!Dispatcher.UIThread.CheckAccess()) {
|
||||||
|
return await Dispatcher.UIThread.InvokeAsync(() => ShowYesNo(owner, title, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await new MessageDialog {
|
||||||
|
DataContext = new MessageDialogModel {
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
IsYesVisible = true,
|
||||||
|
IsNoVisible = true
|
||||||
|
}
|
||||||
|
}.ShowDialog<DialogResult.All?>(owner);
|
||||||
|
|
||||||
|
return result.ToYesNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DialogResult.YesNoCancel> ShowYesNoCancel(Window owner, string title, string message) {
|
||||||
|
if (!Dispatcher.UIThread.CheckAccess()) {
|
||||||
|
return await Dispatcher.UIThread.InvokeAsync(() => ShowYesNoCancel(owner, title, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await new MessageDialog {
|
||||||
|
DataContext = new MessageDialogModel {
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
IsYesVisible = true,
|
||||||
|
IsNoVisible = true,
|
||||||
|
IsCancelVisible = true
|
||||||
|
}
|
||||||
|
}.ShowDialog<DialogResult.All?>(owner);
|
||||||
|
|
||||||
|
return result.ToYesNoCancel();
|
||||||
|
}
|
||||||
|
}
|
59
app/Desktop/App/Dialogs/Message/DialogResult.cs
Normal file
59
app/Desktop/App/Dialogs/Message/DialogResult.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.Message;
|
||||||
|
|
||||||
|
static class DialogResult {
|
||||||
|
public enum All {
|
||||||
|
Ok,
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
|
Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OkCancel {
|
||||||
|
Closed,
|
||||||
|
Ok,
|
||||||
|
Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum YesNo {
|
||||||
|
Closed,
|
||||||
|
Yes,
|
||||||
|
No
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum YesNoCancel {
|
||||||
|
Closed,
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
|
Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OkCancel ToOkCancel(this All? result) {
|
||||||
|
return result switch {
|
||||||
|
null => OkCancel.Closed,
|
||||||
|
All.Ok => OkCancel.Ok,
|
||||||
|
All.Cancel => OkCancel.Cancel,
|
||||||
|
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to ok/cancel.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static YesNo ToYesNo(this All? result) {
|
||||||
|
return result switch {
|
||||||
|
null => YesNo.Closed,
|
||||||
|
All.Yes => YesNo.Yes,
|
||||||
|
All.No => YesNo.No,
|
||||||
|
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to yes/no.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static YesNoCancel ToYesNoCancel(this All? result) {
|
||||||
|
return result switch {
|
||||||
|
null => YesNoCancel.Closed,
|
||||||
|
All.Yes => YesNoCancel.Yes,
|
||||||
|
All.No => YesNoCancel.No,
|
||||||
|
All.Cancel => YesNoCancel.Cancel,
|
||||||
|
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to yes/no/cancel.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -2,15 +2,16 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:dialogs="clr-namespace:DHT.Desktop.Dialogs"
|
xmlns:message="clr-namespace:DHT.Desktop.App.Dialogs.Message"
|
||||||
mc:Ignorable="d" d:DesignWidth="500"
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
x:Class="DHT.Desktop.Dialogs.MessageDialog"
|
x:Class="DHT.Desktop.App.Dialogs.Message.MessageDialog"
|
||||||
Title="{Binding Title}"
|
Title="{Binding Title}"
|
||||||
Width="500" SizeToContent="Height"
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
<Window.DataContext>
|
<Window.DataContext>
|
||||||
<dialogs:MessageDialogModel />
|
<message:MessageDialogModel />
|
||||||
</Window.DataContext>
|
</Window.DataContext>
|
||||||
|
|
||||||
<Window.Styles>
|
<Window.Styles>
|
28
app/Desktop/App/Dialogs/Message/MessageDialog.axaml.cs
Normal file
28
app/Desktop/App/Dialogs/Message/MessageDialog.axaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.Message;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class MessageDialog : Window {
|
||||||
|
public MessageDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.All.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickYes(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.All.Yes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickNo(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.All.No);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickCancel(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.All.Cancel);
|
||||||
|
}
|
||||||
|
}
|
11
app/Desktop/App/Dialogs/Message/MessageDialogModel.cs
Normal file
11
app/Desktop/App/Dialogs/Message/MessageDialogModel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace DHT.Desktop.App.Dialogs.Message;
|
||||||
|
|
||||||
|
sealed class MessageDialogModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
public string Message { get; init; } = "";
|
||||||
|
|
||||||
|
public bool IsOkVisible { get; init; } = false;
|
||||||
|
public bool IsYesVisible { get; init; } = false;
|
||||||
|
public bool IsNoVisible { get; init; } = false;
|
||||||
|
public bool IsCancelVisible { get; init; } = false;
|
||||||
|
}
|
7
app/Desktop/App/Dialogs/Progress/IProgressCallback.cs
Normal file
7
app/Desktop/App/Dialogs/Progress/IProgressCallback.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.Progress;
|
||||||
|
|
||||||
|
interface IProgressCallback {
|
||||||
|
Task Update(string message, int finishedItems, int totalItems);
|
||||||
|
}
|
42
app/Desktop/App/Dialogs/Progress/ProgressDialog.axaml
Normal file
42
app/Desktop/App/Dialogs/Progress/ProgressDialog.axaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:progress="clr-namespace:DHT.Desktop.App.Dialogs.Progress"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.App.Dialogs.Progress.ProgressDialog"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Opened="OnOpened"
|
||||||
|
Closing="OnClosing"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<progress:ProgressDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="15" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.items">
|
||||||
|
<Setter Property="Margin" Value="10 0 0 0" />
|
||||||
|
<Setter Property="TextAlignment" Value="Right" />
|
||||||
|
<Setter Property="TextWrapping" Value="NoWrap" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ProgressBar">
|
||||||
|
<Setter Property="Margin" Value="0 15 0 0" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="{Binding Items}" Classes="items" />
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="{Binding Message}" />
|
||||||
|
</DockPanel>
|
||||||
|
<ProgressBar Value="{Binding Progress}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Window>
|
30
app/Desktop/App/Dialogs/Progress/ProgressDialog.axaml.cs
Normal file
30
app/Desktop/App/Dialogs/Progress/ProgressDialog.axaml.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.Progress;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class ProgressDialog : Window {
|
||||||
|
private bool isFinished = false;
|
||||||
|
|
||||||
|
public ProgressDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnOpened(object? sender, EventArgs e) {
|
||||||
|
if (DataContext is ProgressDialogModel model) {
|
||||||
|
Task.Run(model.StartTask).ContinueWith(OnFinished, TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClosing(object? sender, WindowClosingEventArgs e) {
|
||||||
|
e.Cancel = !isFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFinished(Task task) {
|
||||||
|
isFinished = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
65
app/Desktop/App/Dialogs/Progress/ProgressDialogModel.cs
Normal file
65
app/Desktop/App/Dialogs/Progress/ProgressDialogModel.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Desktop.App.Common;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.Progress;
|
||||||
|
|
||||||
|
sealed class ProgressDialogModel : BaseModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
|
||||||
|
private string message = "";
|
||||||
|
|
||||||
|
public string Message {
|
||||||
|
get => message;
|
||||||
|
private set => Change(ref message, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string items = "";
|
||||||
|
|
||||||
|
public string Items {
|
||||||
|
get => items;
|
||||||
|
private set => Change(ref items, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int progress = 0;
|
||||||
|
|
||||||
|
public int Progress {
|
||||||
|
get => progress;
|
||||||
|
private set => Change(ref progress, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly TaskRunner? task;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public ProgressDialogModel() {}
|
||||||
|
|
||||||
|
public ProgressDialogModel(TaskRunner task) {
|
||||||
|
this.task = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task StartTask() {
|
||||||
|
if (task != null) {
|
||||||
|
await task(new Callback(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate Task TaskRunner(IProgressCallback callback);
|
||||||
|
|
||||||
|
private sealed class Callback : IProgressCallback {
|
||||||
|
private readonly ProgressDialogModel model;
|
||||||
|
|
||||||
|
public Callback(ProgressDialogModel model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task IProgressCallback.Update(string message, int finishedItems, int totalItems) {
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
model.Message = message;
|
||||||
|
model.Items = finishedItems.Format() + " / " + totalItems.Format();
|
||||||
|
model.Progress = 100 * finishedItems / totalItems;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
app/Desktop/App/Dialogs/TextBox/TextBoxDialog.axaml
Normal file
56
app/Desktop/App/Dialogs/TextBox/TextBoxDialog.axaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:textBox="clr-namespace:DHT.Desktop.App.Dialogs.TextBox"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.App.Dialogs.TextBox.TextBoxDialog"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<textBox:TextBoxDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Panel.buttons">
|
||||||
|
<Setter Property="Margin" Value="0 20 0 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons > WrapPanel.right">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons Button">
|
||||||
|
<Setter Property="MinWidth" Value="80" />
|
||||||
|
<Setter Property="Margin" Value="8 0 0 0" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<ScrollViewer MaxHeight="400">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
|
||||||
|
<ItemsRepeater ItemsSource="{Binding Items}">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<DockPanel Margin="0 5 25 0">
|
||||||
|
<TextBox Name="Input" Text="{Binding Value}" Width="180" VerticalAlignment="Top" DockPanel.Dock="Right" />
|
||||||
|
<Label Target="Input" VerticalAlignment="Center" DockPanel.Dock="Left">
|
||||||
|
<TextBlock Text="{Binding Title}" TextWrapping="Wrap" />
|
||||||
|
</Label>
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
<Panel Classes="buttons">
|
||||||
|
<WrapPanel Classes="right">
|
||||||
|
<Button Click="ClickOk" IsEnabled="{Binding !HasErrors}">OK</Button>
|
||||||
|
<Button Click="ClickCancel">Cancel</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Window>
|
21
app/Desktop/App/Dialogs/TextBox/TextBoxDialog.axaml.cs
Normal file
21
app/Desktop/App/Dialogs/TextBox/TextBoxDialog.axaml.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.TextBox;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class TextBoxDialog : Window {
|
||||||
|
public TextBoxDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.OkCancel.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickCancel(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.OkCancel.Cancel);
|
||||||
|
}
|
||||||
|
}
|
47
app/Desktop/App/Dialogs/TextBox/TextBoxDialogModel.cs
Normal file
47
app/Desktop/App/Dialogs/TextBox/TextBoxDialogModel.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.TextBox;
|
||||||
|
|
||||||
|
class TextBoxDialogModel : BaseModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
public string Description { get; init; } = "";
|
||||||
|
|
||||||
|
private IReadOnlyList<TextBoxItem> items = Array.Empty<TextBoxItem>();
|
||||||
|
|
||||||
|
public IReadOnlyList<TextBoxItem> Items {
|
||||||
|
get => items;
|
||||||
|
|
||||||
|
protected set {
|
||||||
|
foreach (var item in items) {
|
||||||
|
item.ErrorsChanged -= OnItemErrorsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = value;
|
||||||
|
|
||||||
|
foreach (var item in items) {
|
||||||
|
item.ErrorsChanged += OnItemErrorsChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasErrors => Items.Any(static item => !item.IsValid);
|
||||||
|
|
||||||
|
private void OnItemErrorsChanged(object? sender, DataErrorsChangedEventArgs e) {
|
||||||
|
OnPropertyChanged(nameof(HasErrors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class TextBoxDialogModel<T> : TextBoxDialogModel {
|
||||||
|
public new IReadOnlyList<TextBoxItem<T>> Items { get; }
|
||||||
|
|
||||||
|
public IEnumerable<TextBoxItem<T>> ValidItems => Items.Where(static item => item.IsValid);
|
||||||
|
|
||||||
|
public TextBoxDialogModel(IEnumerable<TextBoxItem<T>> items) {
|
||||||
|
this.Items = new List<TextBoxItem<T>>(items);
|
||||||
|
base.Items = this.Items;
|
||||||
|
}
|
||||||
|
}
|
42
app/Desktop/App/Dialogs/TextBox/TextBoxItem.cs
Normal file
42
app/Desktop/App/Dialogs/TextBox/TextBoxItem.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Dialogs.TextBox;
|
||||||
|
|
||||||
|
class TextBoxItem : BaseModel, INotifyDataErrorInfo {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
public object? Item { get; init; } = null;
|
||||||
|
|
||||||
|
public Func<string, bool> ValidityCheck { get; init; } = static _ => true;
|
||||||
|
public bool IsValid => ValidityCheck(Value);
|
||||||
|
|
||||||
|
private string value = string.Empty;
|
||||||
|
|
||||||
|
public string Value {
|
||||||
|
get => this.value;
|
||||||
|
set {
|
||||||
|
Change(ref this.value, value);
|
||||||
|
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable GetErrors(string? propertyName) {
|
||||||
|
if (propertyName == nameof(Value) && !IsValid) {
|
||||||
|
yield return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasErrors => !IsValid;
|
||||||
|
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class TextBoxItem<T> : TextBoxItem {
|
||||||
|
public new T Item { get; }
|
||||||
|
|
||||||
|
public TextBoxItem(T item) {
|
||||||
|
this.Item = item;
|
||||||
|
base.Item = item;
|
||||||
|
}
|
||||||
|
}
|
25
app/Desktop/App/Pages/AdvancedPage.axaml
Normal file
25
app/Desktop/App/Pages/AdvancedPage.axaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:pages="clr-namespace:DHT.Desktop.App.Pages"
|
||||||
|
xmlns:controls="clr-namespace:DHT.Desktop.App.Controls"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.App.Pages.AdvancedPage">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:AdvancedPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<Expander Header="Internal Server Configuration" IsExpanded="True">
|
||||||
|
<controls:ServerConfigurationPanel DataContext="{Binding ServerConfigurationModel}" />
|
||||||
|
</Expander>
|
||||||
|
<Expander Header="Database Tools" IsExpanded="True">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<TextBlock TextWrapping="Wrap">Recreates the database to remove any traces of deleted data, which frees up space and prevents forensic recovery.</TextBlock>
|
||||||
|
<Button Command="{Binding VacuumDatabase}">Vacuum Database</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
11
app/Desktop/App/Pages/AdvancedPage.axaml.cs
Normal file
11
app/Desktop/App/Pages/AdvancedPage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class AdvancedPage : UserControl {
|
||||||
|
public AdvancedPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
39
app/Desktop/App/Pages/AdvancedPageModel.cs
Normal file
39
app/Desktop/App/Pages/AdvancedPageModel.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Controls;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Pages;
|
||||||
|
|
||||||
|
sealed class AdvancedPageModel : BaseModel, IDisposable {
|
||||||
|
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public AdvancedPageModel() : this(null!, DummyDatabaseFile.Instance, new ServerManager(DummyDatabaseFile.Instance)) {}
|
||||||
|
|
||||||
|
public AdvancedPageModel(Window window, IDatabaseFile db, ServerManager serverManager) {
|
||||||
|
this.window = window;
|
||||||
|
this.db = db;
|
||||||
|
|
||||||
|
ServerConfigurationModel = new ServerConfigurationPanelModel(window, serverManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize() {
|
||||||
|
ServerConfigurationModel.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
ServerConfigurationModel.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void VacuumDatabase() {
|
||||||
|
db.Vacuum();
|
||||||
|
await Dialog.ShowOk(window, "Vacuum Database", "Done.");
|
||||||
|
}
|
||||||
|
}
|
@@ -2,20 +2,28 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:pages="clr-namespace:DHT.Desktop.Main.Pages"
|
xmlns:pages="clr-namespace:DHT.Desktop.App.Pages"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="DHT.Desktop.Main.Pages.DatabasePage">
|
x:Class="DHT.Desktop.App.Pages.DatabasePage">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<pages:DatabasePageModel />
|
<pages:DatabasePageModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="WrapPanel > Button">
|
||||||
|
<Setter Property="Margin" Value="0 0 10 10" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
<StackPanel Spacing="10">
|
<StackPanel Spacing="10">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Button Command="{Binding CloseDatabase}" DockPanel.Dock="Right">Close Database</Button>
|
<Button Command="{Binding CloseDatabase}" DockPanel.Dock="Right">Close Database</Button>
|
||||||
<TextBox Text="{Binding Db.Path}" Width="NaN" Margin="0 0 10 0" IsEnabled="True" />
|
<TextBox Text="{Binding Db.Path}" Width="NaN" Margin="0 0 10 0" IsReadOnly="True" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>
|
<WrapPanel>
|
||||||
|
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>
|
||||||
|
</WrapPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
11
app/Desktop/App/Pages/DatabasePage.axaml.cs
Normal file
11
app/Desktop/App/Pages/DatabasePage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class DatabasePage : UserControl {
|
||||||
|
public DatabasePage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
59
app/Desktop/App/Pages/DatabasePageModel.cs
Normal file
59
app/Desktop/App/Pages/DatabasePageModel.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Pages;
|
||||||
|
|
||||||
|
sealed class DatabasePageModel : BaseModel {
|
||||||
|
private static readonly Log Log = Log.ForType<DatabasePageModel>();
|
||||||
|
|
||||||
|
public IDatabaseFile Db { get; }
|
||||||
|
|
||||||
|
public event EventHandler? DatabaseClosed;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public DatabasePageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
||||||
|
|
||||||
|
public DatabasePageModel(Window window, IDatabaseFile db) {
|
||||||
|
this.window = window;
|
||||||
|
this.Db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OpenDatabaseFolder() {
|
||||||
|
string file = Db.Path;
|
||||||
|
string? folder = Path.GetDirectoryName(file);
|
||||||
|
|
||||||
|
if (folder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (Environment.OSVersion.Platform) {
|
||||||
|
case PlatformID.Win32NT:
|
||||||
|
Process.Start("explorer.exe", "/select,\"" + file + "\"");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlatformID.Unix:
|
||||||
|
Process.Start("xdg-open", new string[] { folder });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlatformID.MacOSX:
|
||||||
|
Process.Start("open", new string[] { folder });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
await Dialog.ShowOk(window, "Feature Not Supported", "This feature is not supported for your operating system.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseDatabase() {
|
||||||
|
DatabaseClosed?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
26
app/Desktop/App/Pages/TrackingPage.axaml
Normal file
26
app/Desktop/App/Pages/TrackingPage.axaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:pages="clr-namespace:DHT.Desktop.App.Pages"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.App.Pages.TrackingPage">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:TrackingPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock TextWrapping="Wrap">
|
||||||
|
To start tracking messages, copy the tracking script and paste it into the console of either the Discord app, or your browser. The console is usually opened by pressing Ctrl+Shift+I.
|
||||||
|
</TextBlock>
|
||||||
|
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button x:Name="CopyTrackingScript" Click="CopyTrackingScriptButton_OnClick">Copy Tracking Script</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0 5 0 0">
|
||||||
|
By default, the Discord app does not allow opening the console. The button below will change a hidden setting in the Discord app that controls whether the Ctrl+Shift+I shortcut is enabled.
|
||||||
|
</TextBlock>
|
||||||
|
<Button DockPanel.Dock="Right" Command="{Binding OnClickToggleAppDevTools}" Content="{Binding ToggleAppDevToolsButtonText}" IsEnabled="{Binding IsToggleAppDevToolsButtonEnabled}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
32
app/Desktop/App/Pages/TrackingPage.axaml.cs
Normal file
32
app/Desktop/App/Pages/TrackingPage.axaml.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class TrackingPage : UserControl {
|
||||||
|
private bool isCopyingScript;
|
||||||
|
|
||||||
|
public TrackingPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void CopyTrackingScriptButton_OnClick(object? sender, RoutedEventArgs e) {
|
||||||
|
if (DataContext is TrackingPageModel model) {
|
||||||
|
var originalText = CopyTrackingScript.Content;
|
||||||
|
CopyTrackingScript.MinWidth = CopyTrackingScript.Bounds.Width;
|
||||||
|
|
||||||
|
if (await model.OnClickCopyTrackingScript() && !isCopyingScript) {
|
||||||
|
isCopyingScript = true;
|
||||||
|
CopyTrackingScript.Content = "Script Copied!";
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||||
|
CopyTrackingScript.Content = originalText;
|
||||||
|
isCopyingScript = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
app/Desktop/App/Pages/TrackingPageModel.cs
Normal file
115
app/Desktop/App/Pages/TrackingPageModel.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Discord;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Pages;
|
||||||
|
|
||||||
|
sealed class TrackingPageModel : BaseModel {
|
||||||
|
private bool areDevToolsEnabled;
|
||||||
|
|
||||||
|
private bool AreDevToolsEnabled {
|
||||||
|
get => areDevToolsEnabled;
|
||||||
|
set {
|
||||||
|
Change(ref areDevToolsEnabled, value);
|
||||||
|
OnPropertyChanged(nameof(ToggleAppDevToolsButtonText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsToggleAppDevToolsButtonEnabled { get; private set; } = true;
|
||||||
|
|
||||||
|
public string ToggleAppDevToolsButtonText {
|
||||||
|
get {
|
||||||
|
if (!IsToggleAppDevToolsButtonEnabled) {
|
||||||
|
return "Unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
|
return AreDevToolsEnabled ? "Disable Ctrl+Shift+I" : "Enable Ctrl+Shift+I";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public TrackingPageModel() : this(null!) {}
|
||||||
|
|
||||||
|
public TrackingPageModel(Window window) {
|
||||||
|
this.window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize() {
|
||||||
|
bool? devToolsEnabled = await DiscordAppSettings.AreDevToolsEnabled();
|
||||||
|
if (devToolsEnabled.HasValue) {
|
||||||
|
AreDevToolsEnabled = devToolsEnabled.Value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
IsToggleAppDevToolsButtonEnabled = false;
|
||||||
|
OnPropertyChanged(nameof(IsToggleAppDevToolsButtonEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> OnClickCopyTrackingScript() {
|
||||||
|
string bootstrap = await Resources.ReadTextAsync("Tracker/bootstrap.js");
|
||||||
|
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + ServerManager.Port + ";")
|
||||||
|
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(ServerManager.Token))
|
||||||
|
.Replace("/*[IMPORTS]*/", await Resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
||||||
|
.Replace("/*[CSS-CONTROLLER]*/", await Resources.ReadTextAsync("Tracker/styles/controller.css"))
|
||||||
|
.Replace("/*[CSS-SETTINGS]*/", await Resources.ReadTextAsync("Tracker/styles/settings.css"));
|
||||||
|
|
||||||
|
var clipboard = window.Clipboard;
|
||||||
|
if (clipboard == null) {
|
||||||
|
await Dialog.ShowOk(window, "Copy Tracking Script", "Clipboard is not available on this system.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await clipboard.SetTextAsync(script);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
await Dialog.ShowOk(window, "Copy Tracking Script", "An error occurred while copying to clipboard.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OnClickToggleAppDevTools() {
|
||||||
|
const string DialogTitle = "Discord App Settings File";
|
||||||
|
|
||||||
|
bool oldState = AreDevToolsEnabled;
|
||||||
|
bool newState = !oldState;
|
||||||
|
|
||||||
|
switch (await DiscordAppSettings.ConfigureDevTools(newState)) {
|
||||||
|
case SettingsJsonResult.Success:
|
||||||
|
AreDevToolsEnabled = newState;
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, "Ctrl+Shift+I was " + (newState ? "enabled." : "disabled.") + " Restart the Discord app for the change to take effect.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingsJsonResult.AlreadySet:
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, "Ctrl+Shift+I is already " + (newState ? "enabled." : "disabled."));
|
||||||
|
AreDevToolsEnabled = newState;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingsJsonResult.FileNotFound:
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, "Cannot find the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingsJsonResult.ReadError:
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, "Cannot read the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingsJsonResult.InvalidJson:
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, "Unknown format of the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingsJsonResult.WriteError:
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, "Cannot save the settings file:\n" + DiscordAppSettings.JsonFilePath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,20 +2,30 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
xmlns:screens="clr-namespace:DHT.Desktop.App.Screens"
|
||||||
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
xmlns:controls="clr-namespace:DHT.Desktop.App.Controls"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="DHT.Desktop.Main.MainContentScreen">
|
x:Class="DHT.Desktop.App.Screens.MainContentScreen">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<main:MainContentScreenModel />
|
<screens:MainContentScreenModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="TabControl WrapPanel">
|
<Style Selector="Border.statusBar">
|
||||||
|
<Setter Property="Background" Value="#3C4F79" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.invisibleTabItem">
|
||||||
|
<Setter Property="FontSize" Value="20" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiLight" />
|
||||||
|
<Setter Property="Margin" Value="17 0" />
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabControl ItemsPresenter > Grid">
|
||||||
<Setter Property="Background" Value="#546A9F" />
|
<Setter Property="Background" Value="#546A9F" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TabItem">
|
<Style Selector="TabItem">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
<Setter Property="Foreground" Value="#E9E9E9" />
|
<Setter Property="Foreground" Value="#E9E9E9" />
|
||||||
<Setter Property="FontSize" Value="20" />
|
<Setter Property="FontSize" Value="20" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -23,10 +33,11 @@
|
|||||||
<Setter Property="Margin" Value="5 0" />
|
<Setter Property="Margin" Value="5 0" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TabItem:pointerover">
|
<Style Selector="TabItem:pointerover">
|
||||||
<Setter Property="Background" Value="#1F2E45" />
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Background" Value="#455785" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TabItem:pointerover /template/ Border">
|
<Style Selector="TabItem:pointerover /template/ Border">
|
||||||
<Setter Property="Background" Value="#1F2E45" />
|
<Setter Property="Background" Value="#455785" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TabItem:pointerover > TextBlock">
|
<Style Selector="TabItem:pointerover > TextBlock">
|
||||||
<Setter Property="Foreground" Value="#E9E9E9" />
|
<Setter Property="Foreground" Value="#E9E9E9" />
|
||||||
@@ -55,35 +66,37 @@
|
|||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ContentPresenter.page">
|
<Style Selector="ContentPresenter.page">
|
||||||
<Setter Property="Margin" Value="15 21" />
|
<Setter Property="Margin" Value="15 21 20 21" />
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TabControl x:Name="TabControl" TabStripPlacement="Left">
|
<Border Classes="statusBar" DockPanel.Dock="Bottom">
|
||||||
<TabItem x:Name="TabDatabase" Header="Database" Classes="first">
|
<DockPanel>
|
||||||
<DockPanel>
|
<TextBlock Classes="invisibleTabItem" DockPanel.Dock="Left">Attachments</TextBlock>
|
||||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Bottom" />
|
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Right" />
|
||||||
<ScrollViewer>
|
</DockPanel>
|
||||||
<ContentPresenter Content="{Binding DatabasePage}" Classes="page" />
|
</Border>
|
||||||
</ScrollViewer>
|
<TabControl x:Name="TabControl" TabStripPlacement="Left" DockPanel.Dock="Top">
|
||||||
</DockPanel>
|
<TabControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Grid ColumnDefinitions="Auto" RowDefinitions="Auto,Auto,*,Auto" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</TabControl.ItemsPanel>
|
||||||
|
<TabItem x:Name="TabDatabase" Header="Database" Classes="first" Grid.Row="0">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ContentPresenter Content="{Binding DatabasePage}" Classes="page" />
|
||||||
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="TabTracking" Header="Tracking">
|
<TabItem x:Name="TabTracking" Header="Tracking" Grid.Row="1">
|
||||||
<DockPanel>
|
<ScrollViewer>
|
||||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Bottom" />
|
<ContentPresenter Content="{Binding TrackingPage}" Classes="page" />
|
||||||
<ScrollViewer>
|
</ScrollViewer>
|
||||||
<ContentPresenter Content="{Binding TrackingPage}" Classes="page" />
|
|
||||||
</ScrollViewer>
|
|
||||||
</DockPanel>
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem x:Name="TabViewer" Header="Viewer">
|
<TabItem x:Name="TabAdvanced" Header="Advanced" Grid.Row="3">
|
||||||
<DockPanel>
|
<ScrollViewer>
|
||||||
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Bottom" />
|
<ContentPresenter Content="{Binding AdvancedPage}" Classes="page" />
|
||||||
<ScrollViewer>
|
</ScrollViewer>
|
||||||
<ContentPresenter Content="{Binding ViewerPage}" Classes="page" />
|
|
||||||
</ScrollViewer>
|
|
||||||
</DockPanel>
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</DockPanel>
|
</DockPanel>
|
11
app/Desktop/App/Screens/MainContentScreen.axaml.cs
Normal file
11
app/Desktop/App/Screens/MainContentScreen.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Screens;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class MainContentScreen : UserControl {
|
||||||
|
public MainContentScreen() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
88
app/Desktop/App/Screens/MainContentScreenModel.cs
Normal file
88
app/Desktop/App/Screens/MainContentScreenModel.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Controls;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Desktop.App.Pages;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Screens;
|
||||||
|
|
||||||
|
sealed class MainContentScreenModel : IDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
|
||||||
|
|
||||||
|
public DatabasePage DatabasePage { get; }
|
||||||
|
private DatabasePageModel DatabasePageModel { get; }
|
||||||
|
|
||||||
|
public TrackingPage TrackingPage { get; }
|
||||||
|
private TrackingPageModel TrackingPageModel { get; }
|
||||||
|
|
||||||
|
public AdvancedPage AdvancedPage { get; }
|
||||||
|
private AdvancedPageModel AdvancedPageModel { get; }
|
||||||
|
|
||||||
|
public StatusBarModel StatusBarModel { get; }
|
||||||
|
|
||||||
|
public event EventHandler? DatabaseClosed {
|
||||||
|
add {
|
||||||
|
DatabasePageModel.DatabaseClosed += value;
|
||||||
|
}
|
||||||
|
remove {
|
||||||
|
DatabasePageModel.DatabaseClosed -= value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly ServerManager serverManager;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public MainContentScreenModel() : this(null!, DummyDatabaseFile.Instance) {}
|
||||||
|
|
||||||
|
public MainContentScreenModel(Window window, IDatabaseFile db) {
|
||||||
|
this.window = window;
|
||||||
|
this.serverManager = new ServerManager(db);
|
||||||
|
|
||||||
|
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
|
||||||
|
|
||||||
|
DatabasePageModel = new DatabasePageModel(window, db);
|
||||||
|
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
||||||
|
|
||||||
|
TrackingPageModel = new TrackingPageModel(window);
|
||||||
|
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
||||||
|
|
||||||
|
AdvancedPageModel = new AdvancedPageModel(window, db, serverManager);
|
||||||
|
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
||||||
|
|
||||||
|
StatusBarModel = new StatusBarModel();
|
||||||
|
|
||||||
|
AdvancedPageModel.ServerConfigurationModel.ServerStatusChanged += OnServerStatusChanged;
|
||||||
|
DatabaseClosed += OnDatabaseClosed;
|
||||||
|
|
||||||
|
StatusBarModel.CurrentStatus = serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize() {
|
||||||
|
await TrackingPageModel.Initialize();
|
||||||
|
AdvancedPageModel.Initialize();
|
||||||
|
serverManager.Launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
||||||
|
serverManager.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServerStatusChanged(object? sender, StatusBarModel.Status e) {
|
||||||
|
StatusBarModel.CurrentStatus = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDatabaseClosed(object? sender, EventArgs e) {
|
||||||
|
serverManager.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
|
||||||
|
Log.Error(ex);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,12 +2,12 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
xmlns:screens="clr-namespace:DHT.Desktop.App.Screens"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="DHT.Desktop.Main.WelcomeScreen">
|
x:Class="DHT.Desktop.App.Screens.WelcomeScreen">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<main:WelcomeScreenModel />
|
<screens:WelcomeScreenModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<UserControl.Background>
|
<UserControl.Background>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<Panel Name="RootPanel">
|
<Panel Name="RootPanel">
|
||||||
<StackPanel Margin="42">
|
<StackPanel Margin="42">
|
||||||
<TextBlock Text="{Binding Version, StringFormat=Discord History Tracker v.{0}}" FontSize="25" Margin="0 0 0 30" HorizontalAlignment="Center" />
|
<TextBlock Text="{Binding Version, StringFormat=Discord History Tracker v{0}}" FontSize="25" Margin="0 0 0 30" HorizontalAlignment="Center" />
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
<Button Command="{Binding OpenOrCreateDatabase}">Open or Create Database</Button>
|
<Button Command="{Binding OpenOrCreateDatabase}">Open or Create Database</Button>
|
11
app/Desktop/App/Screens/WelcomeScreen.axaml.cs
Normal file
11
app/Desktop/App/Screens/WelcomeScreen.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Screens;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class WelcomeScreen : UserControl {
|
||||||
|
public WelcomeScreen() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
71
app/Desktop/App/Screens/WelcomeScreenModel.cs
Normal file
71
app/Desktop/App/Screens/WelcomeScreenModel.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Common;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Desktop.App.Windows;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Screens;
|
||||||
|
|
||||||
|
sealed class WelcomeScreenModel : BaseModel, IDisposable {
|
||||||
|
public string Version => Program.Version;
|
||||||
|
|
||||||
|
public IDatabaseFile? Db { get; private set; }
|
||||||
|
public bool HasDatabase => Db != null;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
|
||||||
|
private string? dbFilePath;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public WelcomeScreenModel() : this(null!) {}
|
||||||
|
|
||||||
|
public WelcomeScreenModel(Window window) {
|
||||||
|
this.window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OpenOrCreateDatabase() {
|
||||||
|
var path = await DatabaseGui.NewOpenOrCreateDatabaseFileDialog(window, Path.GetDirectoryName(dbFilePath));
|
||||||
|
if (path != null) {
|
||||||
|
await OpenOrCreateDatabaseFromPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OpenOrCreateDatabaseFromPath(string path) {
|
||||||
|
if (Db != null) {
|
||||||
|
Db = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbFilePath = path;
|
||||||
|
Db = await DatabaseGui.TryOpenOrCreateDatabaseFromPath(path, window, CheckCanUpgradeDatabase);
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Db));
|
||||||
|
OnPropertyChanged(nameof(HasDatabase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CheckCanUpgradeDatabase() {
|
||||||
|
return DialogResult.YesNo.Yes == await DatabaseGui.ShowCanUpgradeDatabaseDialog(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseDatabase() {
|
||||||
|
Dispose();
|
||||||
|
OnPropertyChanged(nameof(Db));
|
||||||
|
OnPropertyChanged(nameof(HasDatabase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ShowAboutDialog() {
|
||||||
|
await new AboutWindow { DataContext = new AboutWindowModel() }.ShowDialog(this.window);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Exit() {
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
Db?.Dispose();
|
||||||
|
Db = null;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,16 +2,16 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
xmlns:windows="clr-namespace:DHT.Desktop.App.Windows"
|
||||||
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="295"
|
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="295"
|
||||||
x:Class="DHT.Desktop.Main.AboutWindow"
|
x:Class="DHT.Desktop.App.Windows.AboutWindow"
|
||||||
Title="About Discord History Tracker"
|
Title="About Discord History Tracker"
|
||||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
Width="480" Height="295" CanResize="False"
|
Width="480" Height="295" CanResize="False"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<main:AboutWindowModel />
|
<windows:AboutWindowModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Window.Styles>
|
<Window.Styles>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<TextBlock Grid.Row="0" Grid.Column="1" FontWeight="Bold">License</TextBlock>
|
<TextBlock Grid.Row="0" Grid.Column="1" FontWeight="Bold">License</TextBlock>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold">Link</TextBlock>
|
<TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold">Link</TextBlock>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0">.NET 5</TextBlock>
|
<TextBlock Grid.Row="2" Grid.Column="0">.NET 8</TextBlock>
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1">MIT</TextBlock>
|
<TextBlock Grid.Row="2" Grid.Column="1">MIT</TextBlock>
|
||||||
<Button Grid.Row="2" Grid.Column="2" Command="{Binding ShowLibraryNetCore}">GitHub</Button>
|
<Button Grid.Row="2" Grid.Column="2" Command="{Binding ShowLibraryNetCore}">GitHub</Button>
|
||||||
|
|
11
app/Desktop/App/Windows/AboutWindow.axaml.cs
Normal file
11
app/Desktop/App/Windows/AboutWindow.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Windows;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class AboutWindow : Window {
|
||||||
|
public AboutWindow() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
33
app/Desktop/App/Windows/AboutWindowModel.cs
Normal file
33
app/Desktop/App/Windows/AboutWindowModel.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Windows;
|
||||||
|
|
||||||
|
sealed class AboutWindowModel {
|
||||||
|
public void ShowOfficialWebsite() {
|
||||||
|
OpenUrl("https://dht.chylex.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowSourceCode() {
|
||||||
|
OpenUrl("https://github.com/chylex/Discord-History-Tracker");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowLibraryAvalonia() {
|
||||||
|
OpenUrl("https://www.nuget.org/packages/Avalonia");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowLibrarySqlite() {
|
||||||
|
OpenUrl("https://www.sqlite.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowLibrarySqliteAdoNet() {
|
||||||
|
OpenUrl("https://www.nuget.org/packages/Microsoft.Data.Sqlite");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowLibraryNetCore() {
|
||||||
|
OpenUrl("https://github.com/dotnet/core");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenUrl(string url) {
|
||||||
|
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
|
||||||
|
}
|
||||||
|
}
|
22
app/Desktop/App/Windows/MainWindow.axaml
Normal file
22
app/Desktop/App/Windows/MainWindow.axaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:windows="clr-namespace:DHT.Desktop.App.Windows"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.App.Windows.MainWindow"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="800" Height="500"
|
||||||
|
MinWidth="520" MinHeight="300"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Closed="OnClosed">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<windows:MainWindowModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<ContentPresenter Content="{Binding CurrentScreen}" />
|
||||||
|
</Panel>
|
||||||
|
</Window>
|
26
app/Desktop/App/Windows/MainWindow.axaml.cs
Normal file
26
app/Desktop/App/Windows/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Windows;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class MainWindow : Window {
|
||||||
|
[UsedImplicitly]
|
||||||
|
public MainWindow() {
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = new MainWindowModel(this, Arguments.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal MainWindow(Arguments args) {
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = new MainWindowModel(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClosed(object? sender, EventArgs e) {
|
||||||
|
if (DataContext is IDisposable disposable) {
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
app/Desktop/App/Windows/MainWindowModel.cs
Normal file
116
app/Desktop/App/Windows/MainWindowModel.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.App.Dialogs.Message;
|
||||||
|
using DHT.Desktop.App.Screens;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.App.Windows;
|
||||||
|
|
||||||
|
sealed class MainWindowModel : BaseModel, IDisposable {
|
||||||
|
private const string DefaultTitle = "Discord History Tracker";
|
||||||
|
|
||||||
|
public string Title { get; private set; } = DefaultTitle;
|
||||||
|
|
||||||
|
public UserControl CurrentScreen { get; private set; }
|
||||||
|
|
||||||
|
private readonly WelcomeScreen welcomeScreen;
|
||||||
|
private readonly WelcomeScreenModel welcomeScreenModel;
|
||||||
|
|
||||||
|
private MainContentScreen? mainContentScreen;
|
||||||
|
private MainContentScreenModel? mainContentScreenModel;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
|
||||||
|
private IDatabaseFile? db;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public MainWindowModel() : this(null!, Arguments.Empty) {}
|
||||||
|
|
||||||
|
public MainWindowModel(Window window, Arguments args) {
|
||||||
|
this.window = window;
|
||||||
|
|
||||||
|
welcomeScreenModel = new WelcomeScreenModel(window);
|
||||||
|
welcomeScreen = new WelcomeScreen { DataContext = welcomeScreenModel };
|
||||||
|
CurrentScreen = welcomeScreen;
|
||||||
|
|
||||||
|
welcomeScreenModel.PropertyChanged += WelcomeScreenModelOnPropertyChanged;
|
||||||
|
|
||||||
|
var dbFile = args.DatabaseFile;
|
||||||
|
if (!string.IsNullOrWhiteSpace(dbFile)) {
|
||||||
|
async void OnWindowOpened(object? o, EventArgs eventArgs) {
|
||||||
|
window.Opened -= OnWindowOpened;
|
||||||
|
|
||||||
|
// https://github.com/AvaloniaUI/Avalonia/issues/3071
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(dbFile)) {
|
||||||
|
await welcomeScreenModel.OpenOrCreateDatabaseFromPath(dbFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await Dialog.ShowOk(window, "Database Error", "Database file not found:\n" + dbFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Opened += OnWindowOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServerPort != null) {
|
||||||
|
ServerManager.Port = args.ServerPort.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServerToken != null) {
|
||||||
|
ServerManager.Token = args.ServerToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(welcomeScreenModel.Db)) {
|
||||||
|
if (mainContentScreenModel != null) {
|
||||||
|
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||||
|
mainContentScreenModel.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
db?.Dispose();
|
||||||
|
db = welcomeScreenModel.Db;
|
||||||
|
|
||||||
|
if (db == null) {
|
||||||
|
Title = DefaultTitle;
|
||||||
|
mainContentScreenModel = null;
|
||||||
|
mainContentScreen = null;
|
||||||
|
CurrentScreen = welcomeScreen;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Title = Path.GetFileName(db.Path) + " - " + DefaultTitle;
|
||||||
|
mainContentScreenModel = new MainContentScreenModel(window, db);
|
||||||
|
await mainContentScreenModel.Initialize();
|
||||||
|
mainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||||
|
mainContentScreen = new MainContentScreen { DataContext = mainContentScreenModel };
|
||||||
|
CurrentScreen = mainContentScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(CurrentScreen));
|
||||||
|
OnPropertyChanged(nameof(Title));
|
||||||
|
|
||||||
|
window.Focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainContentScreenModelOnDatabaseClosed(object? sender, EventArgs e) {
|
||||||
|
welcomeScreenModel.CloseDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
welcomeScreenModel.Dispose();
|
||||||
|
mainContentScreenModel?.Dispose();
|
||||||
|
db?.Dispose();
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,49 +1,64 @@
|
|||||||
using System;
|
using System;
|
||||||
using DHT.Server.Logging;
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
namespace DHT.Desktop {
|
namespace DHT.Desktop;
|
||||||
public class Arguments {
|
|
||||||
public static Arguments Empty => new(Array.Empty<string>());
|
|
||||||
|
|
||||||
public string? DatabaseFile { get; }
|
sealed class Arguments {
|
||||||
public ushort? ServerPort { get; }
|
private static readonly Log Log = Log.ForType<Arguments>();
|
||||||
public string? ServerToken { get; }
|
|
||||||
|
|
||||||
public Arguments(string[] args) {
|
public static Arguments Empty => new(Array.Empty<string>());
|
||||||
for (int i = 0; i < args.Length; i++) {
|
|
||||||
string key = args[i];
|
public string? DatabaseFile { get; }
|
||||||
|
public ushort? ServerPort { get; }
|
||||||
|
public string? ServerToken { get; }
|
||||||
|
|
||||||
|
public Arguments(string[] args) {
|
||||||
|
for (int i = 0; i < args.Length; i++) {
|
||||||
|
string key = args[i];
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "-debug":
|
||||||
|
Log.IsDebugEnabled = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string value;
|
||||||
|
|
||||||
|
if (i == 0 && !key.StartsWith('-')) {
|
||||||
|
value = key;
|
||||||
|
key = "-db";
|
||||||
|
}
|
||||||
|
else if (i >= args.Length - 1) {
|
||||||
|
Log.Warn("Missing value for command line argument: " + key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = args[++i];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "-db":
|
||||||
|
DatabaseFile = value;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "-port": {
|
||||||
|
if (ushort.TryParse(value, out var port)) {
|
||||||
|
ServerPort = port;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.Warn("Invalid port number: " + value);
|
||||||
|
}
|
||||||
|
|
||||||
if (i >= args.Length - 1) {
|
|
||||||
Log.Warn("Missing value for command line argument: " + key);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string value = args[++i];
|
case "-token":
|
||||||
|
ServerToken = value;
|
||||||
|
continue;
|
||||||
|
|
||||||
switch (key) {
|
default:
|
||||||
case "-db":
|
Log.Warn("Unknown command line argument: " + key);
|
||||||
DatabaseFile = value;
|
break;
|
||||||
continue;
|
|
||||||
|
|
||||||
case "-port": {
|
|
||||||
if (ushort.TryParse(value, out var port)) {
|
|
||||||
ServerPort = port;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Log.Warn("Invalid port number: " + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "-token":
|
|
||||||
ServerToken = value;
|
|
||||||
continue;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Log.Warn("Unknown command line argument: " + key);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>DiscordHistoryTracker</AssemblyName>
|
||||||
<RootNamespace>DHT.Desktop</RootNamespace>
|
<RootNamespace>DHT.Desktop</RootNamespace>
|
||||||
<PackageId>DiscordHistoryTracker</PackageId>
|
<PackageId>DiscordHistoryTracker</PackageId>
|
||||||
<Authors>chylex</Authors>
|
<Authors>chylex</Authors>
|
||||||
@@ -11,56 +11,97 @@
|
|||||||
<ApplicationIcon>./Resources/icon.ico</ApplicationIcon>
|
<ApplicationIcon>./Resources/icon.ico</ApplicationIcon>
|
||||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<AssemblyName>DiscordHistoryTracker</AssemblyName>
|
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
|
||||||
<Version>32.1.0.0</Version>
|
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||||
<AssemblyVersion>$(Version)</AssemblyVersion>
|
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||||
<FileVersion>$(Version)</FileVersion>
|
|
||||||
<PackageVersion>$(Version)</PackageVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="0.10.3" />
|
<PackageReference Include="Avalonia" Version="11.0.0" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.3" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0" />
|
||||||
<ProjectReference Include="..\Server\Server.csproj" />
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
|
||||||
</ItemGroup>
|
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0" Condition=" '$(Configuration)' == 'Debug' " />
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.3" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.0" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Windows\MainWindow.axaml.cs">
|
<ProjectReference Include="..\Server\Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\Version.cs" Link="Version.cs" />
|
||||||
|
<Compile Update="App\App.axaml.cs">
|
||||||
|
<DependentUpon>App.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="App\Windows\AboutWindow.axaml.cs">
|
||||||
|
<DependentUpon>AboutWindow.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="App\Windows\MainWindow.axaml.cs">
|
||||||
<DependentUpon>MainWindow.axaml</DependentUpon>
|
<DependentUpon>MainWindow.axaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Dialogs\MessageDialog.axaml.cs">
|
|
||||||
<DependentUpon>MessageDialog.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<UpToDateCheckInput Remove="Pages\DatabasePage.axaml" />
|
|
||||||
<UpToDateCheckInput Remove="Pages\TrackingPage.axaml" />
|
|
||||||
<UpToDateCheckInput Remove="Pages\ViewerPage.axaml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<AvaloniaResource Include="Resources/icon.ico" />
|
||||||
<EmbeddedResource Include="../Resources/Tracker/bootstrap.js">
|
<EmbeddedResource Include="../Resources/Tracker/bootstrap.js">
|
||||||
<LogicalName>Tracker\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
<LogicalName>Tracker\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||||
<Link>Resources/Tracker/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
<Link>Resources/Tracker/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
<Visible>false</Visible>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="../Resources/Tracker/scripts.min/**">
|
<EmbeddedResource Include="../Resources/Tracker/scripts/**" Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<LogicalName>Tracker\scripts\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
<LogicalName>Tracker\scripts\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||||
<Link>Resources/Tracker/scripts/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
<Link>Resources/Tracker/scripts/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
<Visible>false</Visible>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="../Resources/Tracker/styles/**">
|
<EmbeddedResource Include="../Resources/Tracker/styles/**">
|
||||||
<LogicalName>Tracker\styles\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
<LogicalName>Tracker\styles\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||||
<Link>Resources/Tracker/styles/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
<Link>Resources/Tracker/styles/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
<Visible>false</Visible>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="../Resources/Viewer/**">
|
<EmbeddedResource Include="../Resources/Viewer/**">
|
||||||
<LogicalName>Viewer\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
<LogicalName>Viewer\%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||||
<Link>Resources/Viewer/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
<Link>Resources/Viewer/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
<Visible>false</Visible>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<AvaloniaResource Include="Resources/icon.ico" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="App\Controls\ServerConfigurationPanel.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Controls\StatusBar.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Dialogs\CheckBox\CheckBoxDialog.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Dialogs\Message\MessageDialog.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Dialogs\Progress\ProgressDialog.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Dialogs\TextBox\TextBoxDialog.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Pages\AdvancedPage.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Pages\DatabasePage.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Pages\TrackingPage.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Screens\MainContentScreen.axaml" />
|
||||||
|
<AdditionalFiles Include="App\Screens\WelcomeScreen.axaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<UpToDateCheckInput Remove="Main\Pages\AdvancedPage.axaml" />
|
||||||
|
<UpToDateCheckInput Remove="Main\Pages\DatabasePage.axaml" />
|
||||||
|
<UpToDateCheckInput Remove="Main\Pages\TrackingPage.axaml" />
|
||||||
|
<UpToDateCheckInput Remove="Main\Screens\MainContentScreen.axaml" />
|
||||||
|
<UpToDateCheckInput Remove="Main\Screens\WelcomeScreen.axaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Target Name="MinifyResources" BeforeTargets="PrepareForBuild" Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<PropertyGroup>
|
||||||
|
<MinifiedResourceDir>$(ProjectDir)bin/.res/scripts</MinifiedResourceDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<UpToDateCheckInput Include="$(ProjectDir)../Resources/Tracker/scripts/**" Visible="false" />
|
||||||
|
<EmbeddedResource Include="$(MinifiedResourceDir)/discord.js" LogicalName="Tracker\scripts\discord.js" Visible="false" />
|
||||||
|
<EmbeddedResource Include="$(MinifiedResourceDir)/dom.js" LogicalName="Tracker\scripts\dom.js" Visible="false" />
|
||||||
|
<EmbeddedResource Include="$(MinifiedResourceDir)/gui.js" LogicalName="Tracker\scripts\gui.js" Visible="false" />
|
||||||
|
<EmbeddedResource Include="$(MinifiedResourceDir)/settings.js" LogicalName="Tracker\scripts\settings.js" Visible="false" />
|
||||||
|
<EmbeddedResource Include="$(MinifiedResourceDir)/state.js" LogicalName="Tracker\scripts\state.js" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
<RemoveDir Directories="$(ProjectDir)bin/.res/scripts" />
|
||||||
|
<Exec Command="python $(ProjectDir)../Resources/minify.py" WorkingDirectory="$(ProjectDir)../Resources" IgnoreExitCode="false" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -1,56 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Dialogs {
|
|
||||||
public static class Dialog {
|
|
||||||
public static async Task ShowOk(Window owner, string title, string message) {
|
|
||||||
await new MessageDialog {
|
|
||||||
DataContext = new MessageDialogModel {
|
|
||||||
Title = title,
|
|
||||||
Message = message,
|
|
||||||
IsOkVisible = true
|
|
||||||
}
|
|
||||||
}.ShowDialog<DialogResult.All>(owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<DialogResult.OkCancel> ShowOkCancel(Window owner, string title, string message) {
|
|
||||||
var result = await new MessageDialog {
|
|
||||||
DataContext = new MessageDialogModel {
|
|
||||||
Title = title,
|
|
||||||
Message = message,
|
|
||||||
IsOkVisible = true,
|
|
||||||
IsCancelVisible = true
|
|
||||||
}
|
|
||||||
}.ShowDialog<DialogResult.All?>(owner);
|
|
||||||
|
|
||||||
return result.ToOkCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<DialogResult.YesNo> ShowYesNo(Window owner, string title, string message) {
|
|
||||||
var result = await new MessageDialog {
|
|
||||||
DataContext = new MessageDialogModel {
|
|
||||||
Title = title,
|
|
||||||
Message = message,
|
|
||||||
IsYesVisible = true,
|
|
||||||
IsNoVisible = true
|
|
||||||
}
|
|
||||||
}.ShowDialog<DialogResult.All?>(owner);
|
|
||||||
|
|
||||||
return result.ToYesNo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<DialogResult.YesNoCancel> ShowYesNoCancel(Window owner, string title, string message) {
|
|
||||||
var result = await new MessageDialog {
|
|
||||||
DataContext = new MessageDialogModel {
|
|
||||||
Title = title,
|
|
||||||
Message = message,
|
|
||||||
IsYesVisible = true,
|
|
||||||
IsNoVisible = true,
|
|
||||||
IsCancelVisible = true
|
|
||||||
}
|
|
||||||
}.ShowDialog<DialogResult.All?>(owner);
|
|
||||||
|
|
||||||
return result.ToYesNoCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,59 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Dialogs {
|
|
||||||
public static class DialogResult {
|
|
||||||
public enum All {
|
|
||||||
Ok,
|
|
||||||
Yes,
|
|
||||||
No,
|
|
||||||
Cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum OkCancel {
|
|
||||||
Closed,
|
|
||||||
Ok,
|
|
||||||
Cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum YesNo {
|
|
||||||
Closed,
|
|
||||||
Yes,
|
|
||||||
No
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum YesNoCancel {
|
|
||||||
Closed,
|
|
||||||
Yes,
|
|
||||||
No,
|
|
||||||
Cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkCancel ToOkCancel(this All? result) {
|
|
||||||
return result switch {
|
|
||||||
null => OkCancel.Closed,
|
|
||||||
All.Ok => OkCancel.Ok,
|
|
||||||
All.Cancel => OkCancel.Cancel,
|
|
||||||
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to ok/cancel.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YesNo ToYesNo(this All? result) {
|
|
||||||
return result switch {
|
|
||||||
null => YesNo.Closed,
|
|
||||||
All.Yes => YesNo.Yes,
|
|
||||||
All.No => YesNo.No,
|
|
||||||
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to yes/no.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static YesNoCancel ToYesNoCancel(this All? result) {
|
|
||||||
return result switch {
|
|
||||||
null => YesNoCancel.Closed,
|
|
||||||
All.Yes => YesNoCancel.Yes,
|
|
||||||
All.No => YesNoCancel.No,
|
|
||||||
All.Cancel => YesNoCancel.Cancel,
|
|
||||||
_ => throw new ArgumentException("Cannot convert dialog result " + result + " to yes/no/cancel.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Dialogs {
|
|
||||||
public class MessageDialog : Window {
|
|
||||||
public MessageDialog() {
|
|
||||||
InitializeComponent();
|
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClickOk(object? sender, RoutedEventArgs e) {
|
|
||||||
Close(DialogResult.All.Ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClickYes(object? sender, RoutedEventArgs e) {
|
|
||||||
Close(DialogResult.All.Yes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClickNo(object? sender, RoutedEventArgs e) {
|
|
||||||
Close(DialogResult.All.No);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClickCancel(object? sender, RoutedEventArgs e) {
|
|
||||||
Close(DialogResult.All.Cancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
namespace DHT.Desktop.Dialogs {
|
|
||||||
public class MessageDialogModel {
|
|
||||||
public string Title { get; init; } = "";
|
|
||||||
public string Message { get; init; } = "";
|
|
||||||
|
|
||||||
public bool IsOkVisible { get; init; } = false;
|
|
||||||
public bool IsYesVisible { get; init; } = false;
|
|
||||||
public bool IsNoVisible { get; init; } = false;
|
|
||||||
public bool IsCancelVisible { get; init; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
119
app/Desktop/Discord/DiscordAppSettings.cs
Normal file
119
app/Desktop/Discord/DiscordAppSettings.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using static System.Environment.SpecialFolder;
|
||||||
|
using static System.Environment.SpecialFolderOption;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Discord;
|
||||||
|
|
||||||
|
static class DiscordAppSettings {
|
||||||
|
private static readonly Log Log = Log.ForType(typeof(DiscordAppSettings));
|
||||||
|
|
||||||
|
private const string JsonKeyDevTools = "DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING";
|
||||||
|
|
||||||
|
public static string JsonFilePath { get; }
|
||||||
|
private static string JsonBackupFilePath { get; }
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
|
||||||
|
static DiscordAppSettings() {
|
||||||
|
string rootFolder;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||||
|
rootFolder = Path.Combine(Environment.GetFolderPath(ApplicationData, DoNotVerify), "Discord");
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||||
|
rootFolder = Path.Combine(Environment.GetFolderPath(UserProfile, DoNotVerify), "Library", "Application Support", "Discord");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rootFolder = Path.Combine(Environment.GetFolderPath(ApplicationData, DoNotVerify), "discord");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonFilePath = Path.Combine(rootFolder, "settings.json");
|
||||||
|
JsonBackupFilePath = JsonFilePath + ".bak";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool?> AreDevToolsEnabled() {
|
||||||
|
try {
|
||||||
|
return AreDevToolsEnabled(await ReadSettingsJson());
|
||||||
|
} catch (Exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AreDevToolsEnabled(Dictionary<string, object?> json) {
|
||||||
|
return json.TryGetValue(JsonKeyDevTools, out var value) && value is JsonElement { ValueKind: JsonValueKind.True };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<SettingsJsonResult> ConfigureDevTools(bool enable) {
|
||||||
|
Dictionary<string, object?> json;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json = await ReadSettingsJson();
|
||||||
|
} catch (FileNotFoundException) {
|
||||||
|
return SettingsJsonResult.FileNotFound;
|
||||||
|
} catch (JsonException) {
|
||||||
|
return SettingsJsonResult.InvalidJson;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error(e);
|
||||||
|
return SettingsJsonResult.ReadError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable == AreDevToolsEnabled(json)) {
|
||||||
|
return SettingsJsonResult.AlreadySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
json[JsonKeyDevTools] = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
json.Remove(JsonKeyDevTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!File.Exists(JsonBackupFilePath)) {
|
||||||
|
File.Copy(JsonFilePath, JsonBackupFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await WriteSettingsJson(json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("An error occurred when writing settings file.");
|
||||||
|
Log.Error(e);
|
||||||
|
|
||||||
|
if (File.Exists(JsonBackupFilePath)) {
|
||||||
|
try {
|
||||||
|
File.Move(JsonBackupFilePath, JsonFilePath, true);
|
||||||
|
Log.Info("Restored settings file from backup.");
|
||||||
|
} catch (Exception e2) {
|
||||||
|
Log.Error("Cannot restore settings file from backup.");
|
||||||
|
Log.Error(e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsJsonResult.WriteError;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File.Delete(JsonBackupFilePath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Cannot delete backup file.");
|
||||||
|
Log.Error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsJsonResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Dictionary<string, object?>> ReadSettingsJson() {
|
||||||
|
await using var stream = new FileStream(JsonFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
return await JsonSerializer.DeserializeAsync<Dictionary<string, object?>?>(stream) ?? throw new JsonException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WriteSettingsJson(Dictionary<string, object?> json) {
|
||||||
|
await using var stream = new FileStream(JsonFilePath, FileMode.Truncate, FileAccess.Write, FileShare.None);
|
||||||
|
await JsonSerializer.SerializeAsync(stream, json, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
}
|
||||||
|
}
|
10
app/Desktop/Discord/SettingsJsonResult.cs
Normal file
10
app/Desktop/Discord/SettingsJsonResult.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace DHT.Desktop.Discord;
|
||||||
|
|
||||||
|
enum SettingsJsonResult {
|
||||||
|
Success,
|
||||||
|
AlreadySet,
|
||||||
|
FileNotFound,
|
||||||
|
ReadError,
|
||||||
|
InvalidJson,
|
||||||
|
WriteError
|
||||||
|
}
|
@@ -1,18 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class AboutWindow : Window {
|
|
||||||
public AboutWindow() {
|
|
||||||
InitializeComponent();
|
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class AboutWindowModel {
|
|
||||||
public void ShowOfficialWebsite() {
|
|
||||||
OpenUrl("https://dht.chylex.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowSourceCode() {
|
|
||||||
OpenUrl("https://github.com/chylex/Discord-History-Tracker");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowLibraryAvalonia() {
|
|
||||||
OpenUrl("https://www.nuget.org/packages/Avalonia");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowLibrarySqlite() {
|
|
||||||
OpenUrl("https://www.sqlite.org");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowLibrarySqliteAdoNet() {
|
|
||||||
OpenUrl("https://www.nuget.org/packages/Microsoft.Data.Sqlite");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowLibraryNetCore() {
|
|
||||||
OpenUrl("https://github.com/dotnet/core");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OpenUrl(string url) {
|
|
||||||
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Controls {
|
|
||||||
public class StatusBar : UserControl {
|
|
||||||
public StatusBar() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,45 +0,0 @@
|
|||||||
using System;
|
|
||||||
using DHT.Desktop.Models;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Controls {
|
|
||||||
public class StatusBarModel : BaseModel {
|
|
||||||
public DatabaseStatistics DatabaseStatistics { get; }
|
|
||||||
|
|
||||||
private Status status = Status.Stopped;
|
|
||||||
|
|
||||||
public Status CurrentStatus {
|
|
||||||
get => status;
|
|
||||||
set {
|
|
||||||
status = value;
|
|
||||||
OnPropertyChanged(nameof(StatusText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusText {
|
|
||||||
get {
|
|
||||||
return CurrentStatus switch {
|
|
||||||
Status.Starting => "STARTING",
|
|
||||||
Status.Ready => "READY",
|
|
||||||
Status.Stopping => "STOPPING",
|
|
||||||
Status.Stopped => "STOPPED",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public StatusBarModel() : this(new DatabaseStatistics()) {}
|
|
||||||
|
|
||||||
public StatusBarModel(DatabaseStatistics databaseStatistics) {
|
|
||||||
this.DatabaseStatistics = databaseStatistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Status {
|
|
||||||
Starting,
|
|
||||||
Ready,
|
|
||||||
Stopping,
|
|
||||||
Stopped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class MainContentScreen : UserControl {
|
|
||||||
public MainContentScreen() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,57 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using DHT.Desktop.Main.Controls;
|
|
||||||
using DHT.Desktop.Main.Pages;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Service;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class MainContentScreenModel : IDisposable {
|
|
||||||
public DatabasePage DatabasePage { get; }
|
|
||||||
private DatabasePageModel DatabasePageModel { get; }
|
|
||||||
|
|
||||||
public TrackingPage TrackingPage { get; }
|
|
||||||
private TrackingPageModel TrackingPageModel { get; }
|
|
||||||
|
|
||||||
public ViewerPage ViewerPage { get; }
|
|
||||||
private ViewerPageModel ViewerPageModel { get; }
|
|
||||||
|
|
||||||
public StatusBarModel StatusBarModel { get; }
|
|
||||||
|
|
||||||
public event EventHandler? DatabaseClosed {
|
|
||||||
add { DatabasePageModel.DatabaseClosed += value; }
|
|
||||||
remove { DatabasePageModel.DatabaseClosed -= value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public MainContentScreenModel() : this(null!, DummyDatabaseFile.Instance) {}
|
|
||||||
|
|
||||||
public MainContentScreenModel(Window window, IDatabaseFile db) {
|
|
||||||
DatabasePageModel = new DatabasePageModel(window, db);
|
|
||||||
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
|
||||||
|
|
||||||
TrackingPageModel = new TrackingPageModel(window, db);
|
|
||||||
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
|
||||||
|
|
||||||
ViewerPageModel = new ViewerPageModel(window, db);
|
|
||||||
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
|
||||||
|
|
||||||
StatusBarModel = new StatusBarModel(db.Statistics);
|
|
||||||
TrackingPageModel.ServerStatusChanged += TrackingPageModelOnServerStatusChanged;
|
|
||||||
StatusBarModel.CurrentStatus = ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
|
||||||
TrackingPageModel.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TrackingPageModelOnServerStatusChanged(object? sender, StatusBarModel.Status e) {
|
|
||||||
StatusBarModel.CurrentStatus = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
TrackingPageModel.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:main="clr-namespace:DHT.Desktop.Main"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="DHT.Desktop.Main.MainWindow"
|
|
||||||
Title="Discord History Tracker"
|
|
||||||
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
|
||||||
Width="800" Height="450"
|
|
||||||
MinWidth="480" MinHeight="240"
|
|
||||||
WindowStartupLocation="CenterScreen">
|
|
||||||
|
|
||||||
<Design.DataContext>
|
|
||||||
<main:MainWindowModel />
|
|
||||||
</Design.DataContext>
|
|
||||||
|
|
||||||
<Panel>
|
|
||||||
<ContentPresenter Content="{Binding WelcomeScreen}" IsVisible="{Binding ShowWelcomeScreen}" />
|
|
||||||
<ContentPresenter Content="{Binding MainContentScreen}" IsVisible="{Binding ShowMainContentScreen}" />
|
|
||||||
</Panel>
|
|
||||||
</Window>
|
|
@@ -1,26 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class MainWindow : Window {
|
|
||||||
[UsedImplicitly]
|
|
||||||
public MainWindow() {
|
|
||||||
InitializeComponent(Arguments.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MainWindow(Arguments args) {
|
|
||||||
InitializeComponent(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent(Arguments args) {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
DataContext = new MainWindowModel(this, args);
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
this.AttachDevTools();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,101 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using DHT.Desktop.Dialogs;
|
|
||||||
using DHT.Desktop.Main.Pages;
|
|
||||||
using DHT.Desktop.Models;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class MainWindowModel : BaseModel {
|
|
||||||
public WelcomeScreen WelcomeScreen { get; }
|
|
||||||
private WelcomeScreenModel WelcomeScreenModel { get; }
|
|
||||||
|
|
||||||
public MainContentScreen? MainContentScreen { get; private set; }
|
|
||||||
private MainContentScreenModel? MainContentScreenModel { get; set; }
|
|
||||||
|
|
||||||
public bool ShowWelcomeScreen => db == null;
|
|
||||||
public bool ShowMainContentScreen => db != null;
|
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
|
|
||||||
private IDatabaseFile? db;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public MainWindowModel() : this(null!, Arguments.Empty) {}
|
|
||||||
|
|
||||||
public MainWindowModel(Window window, Arguments args) {
|
|
||||||
this.window = window;
|
|
||||||
|
|
||||||
WelcomeScreenModel = new WelcomeScreenModel(window);
|
|
||||||
WelcomeScreen = new WelcomeScreen { DataContext = WelcomeScreenModel };
|
|
||||||
|
|
||||||
WelcomeScreenModel.PropertyChanged += WelcomeScreenModelOnPropertyChanged;
|
|
||||||
|
|
||||||
var dbFile = args.DatabaseFile;
|
|
||||||
if (!string.IsNullOrWhiteSpace(dbFile)) {
|
|
||||||
async void OnWindowOpened(object? o, EventArgs eventArgs) {
|
|
||||||
window.Opened -= OnWindowOpened;
|
|
||||||
|
|
||||||
// https://github.com/AvaloniaUI/Avalonia/issues/3071
|
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
|
||||||
await Task.Delay(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(dbFile)) {
|
|
||||||
await WelcomeScreenModel.OpenOrCreateDatabaseFromPath(dbFile);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await Dialog.ShowOk(window, "Database Error", "Database file not found:\n" + dbFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Opened += OnWindowOpened;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.ServerPort != null) {
|
|
||||||
TrackingPageModel.ServerPort = args.ServerPort.ToString()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.ServerToken != null) {
|
|
||||||
TrackingPageModel.ServerToken = args.ServerToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
|
||||||
if (e.PropertyName == nameof(WelcomeScreenModel.Db)) {
|
|
||||||
if (MainContentScreenModel != null) {
|
|
||||||
MainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
|
||||||
MainContentScreenModel.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
db?.Dispose();
|
|
||||||
db = WelcomeScreenModel.Db;
|
|
||||||
|
|
||||||
if (db == null) {
|
|
||||||
MainContentScreenModel = null;
|
|
||||||
MainContentScreen = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
MainContentScreenModel = new MainContentScreenModel(window, db);
|
|
||||||
MainContentScreenModel.Initialize();
|
|
||||||
MainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
|
||||||
MainContentScreen = new MainContentScreen { DataContext = MainContentScreenModel };
|
|
||||||
OnPropertyChanged(nameof(MainContentScreen));
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ShowWelcomeScreen));
|
|
||||||
OnPropertyChanged(nameof(ShowMainContentScreen));
|
|
||||||
|
|
||||||
window.Focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MainContentScreenModelOnDatabaseClosed(object? sender, EventArgs e) {
|
|
||||||
WelcomeScreenModel.CloseDatabase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages {
|
|
||||||
public class DatabasePage : UserControl {
|
|
||||||
public DatabasePage() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,58 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using DHT.Desktop.Dialogs;
|
|
||||||
using DHT.Desktop.Models;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Service;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages {
|
|
||||||
public class DatabasePageModel : BaseModel {
|
|
||||||
public IDatabaseFile Db { get; }
|
|
||||||
|
|
||||||
public event EventHandler? DatabaseClosed;
|
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public DatabasePageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
|
||||||
|
|
||||||
public DatabasePageModel(Window window, IDatabaseFile db) {
|
|
||||||
this.window = window;
|
|
||||||
this.Db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void OpenDatabaseFolder() {
|
|
||||||
string file = Db.Path;
|
|
||||||
string? folder = Path.GetDirectoryName(file);
|
|
||||||
|
|
||||||
if (folder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (Environment.OSVersion.Platform) {
|
|
||||||
case PlatformID.Win32NT:
|
|
||||||
Process.Start("explorer.exe", "/select,\"" + file + "\"");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlatformID.Unix:
|
|
||||||
Process.Start("xdg-open", new string[] { folder });
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlatformID.MacOSX:
|
|
||||||
Process.Start("open", new string[] { folder });
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
await Dialog.ShowOk(window, "Feature Not Supported", "This feature is not supported for your operating system.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseDatabase() {
|
|
||||||
ServerLauncher.Stop();
|
|
||||||
DatabaseClosed?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:pages="clr-namespace:DHT.Desktop.Main.Pages"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="DHT.Desktop.Main.Pages.TrackingPage">
|
|
||||||
|
|
||||||
<Design.DataContext>
|
|
||||||
<pages:TrackingPageModel />
|
|
||||||
</Design.DataContext>
|
|
||||||
|
|
||||||
<UserControl.Styles>
|
|
||||||
<Style Selector="TextBox">
|
|
||||||
<Setter Property="FontFamily" Value="Consolas,Courier" />
|
|
||||||
<Setter Property="FontSize" Value="15" />
|
|
||||||
</Style>
|
|
||||||
</UserControl.Styles>
|
|
||||||
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock TextWrapping="Wrap">
|
|
||||||
To start tracking messages, copy the tracking script and paste it into the console of either the Discord app (Ctrl+Shift+I), or your browser with Discord open.
|
|
||||||
</TextBlock>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
||||||
<Button x:Name="CopyTrackingScript" Click="CopyTrackingScriptButton_OnClick">Copy Tracking Script</Button>
|
|
||||||
<Button Command="{Binding OnClickToggleButton}" Content="{Binding ToggleButtonText}" IsEnabled="{Binding IsToggleButtonEnabled}" />
|
|
||||||
</StackPanel>
|
|
||||||
<Expander Header="Advanced Settings">
|
|
||||||
<StackPanel Spacing="10" Margin="0 10 0 0">
|
|
||||||
<TextBlock TextWrapping="Wrap">
|
|
||||||
The following settings determine how the tracking script communicates with this application. If you change them, you will have to copy and apply the tracking script again.
|
|
||||||
</TextBlock>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
||||||
<StackPanel Orientation="Vertical">
|
|
||||||
<Label Target="Port">Port</Label>
|
|
||||||
<TextBox x:Name="Port" Width="70" Text="{Binding InputPort}" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Vertical">
|
|
||||||
<Label Target="Token">Token</Label>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBox x:Name="Token" Width="200" Text="{Binding InputToken}" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
<Button Command="{Binding OnClickRandomizeToken}" VerticalAlignment="Bottom">Randomize Token</Button>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
||||||
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickApplyChanges}">Apply & Restart</Button>
|
|
||||||
<Button IsEnabled="{Binding HasMadeChanges}" Command="{Binding OnClickCancelChanges}">Cancel</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Expander>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</UserControl>
|
|
@@ -1,38 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages {
|
|
||||||
public class TrackingPage : UserControl {
|
|
||||||
private bool isCopyingScript;
|
|
||||||
|
|
||||||
public TrackingPage() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void CopyTrackingScriptButton_OnClick(object? sender, RoutedEventArgs e) {
|
|
||||||
if (DataContext is TrackingPageModel model) {
|
|
||||||
var button = this.FindControl<Button>("CopyTrackingScript");
|
|
||||||
var originalText = button.Content;
|
|
||||||
button.MinWidth = button.Bounds.Width;
|
|
||||||
|
|
||||||
await model.OnClickCopyTrackingScript();
|
|
||||||
|
|
||||||
if (!isCopyingScript) {
|
|
||||||
isCopyingScript = true;
|
|
||||||
button.Content = "Script Copied!";
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
|
||||||
button.Content = originalText;
|
|
||||||
isCopyingScript = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,150 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Web;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using DHT.Desktop.Dialogs;
|
|
||||||
using DHT.Desktop.Main.Controls;
|
|
||||||
using DHT.Desktop.Models;
|
|
||||||
using DHT.Desktop.Resources;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Logging;
|
|
||||||
using DHT.Server.Service;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages {
|
|
||||||
public class TrackingPageModel : BaseModel, IDisposable {
|
|
||||||
internal static string ServerPort { get; set; } = ServerUtils.FindAvailablePort(50000, 60000).ToString();
|
|
||||||
internal static string ServerToken { get; set; } = ServerUtils.GenerateRandomToken(20);
|
|
||||||
|
|
||||||
private string inputPort = ServerPort;
|
|
||||||
|
|
||||||
public string InputPort {
|
|
||||||
get => inputPort;
|
|
||||||
set {
|
|
||||||
Change(ref inputPort, value);
|
|
||||||
OnPropertyChanged(nameof(HasMadeChanges));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string inputToken = ServerToken;
|
|
||||||
|
|
||||||
public string InputToken {
|
|
||||||
get => inputToken;
|
|
||||||
set {
|
|
||||||
Change(ref inputToken, value);
|
|
||||||
OnPropertyChanged(nameof(HasMadeChanges));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasMadeChanges => ServerPort != InputPort || ServerToken != InputToken;
|
|
||||||
|
|
||||||
private bool isToggleButtonEnabled = true;
|
|
||||||
|
|
||||||
public bool IsToggleButtonEnabled {
|
|
||||||
get => isToggleButtonEnabled;
|
|
||||||
set => Change(ref isToggleButtonEnabled, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToggleButtonText => ServerLauncher.IsRunning ? "Pause Tracking" : "Resume Tracking";
|
|
||||||
|
|
||||||
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
|
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
private readonly IDatabaseFile db;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public TrackingPageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
|
||||||
|
|
||||||
public TrackingPageModel(Window window, IDatabaseFile db) {
|
|
||||||
this.window = window;
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
|
||||||
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
|
|
||||||
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
|
|
||||||
|
|
||||||
if (int.TryParse(ServerPort, out int port)) {
|
|
||||||
string token = ServerToken;
|
|
||||||
ServerLauncher.Relaunch(port, token, db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
|
||||||
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
|
|
||||||
ServerLauncher.Stop();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> StartServer() {
|
|
||||||
if (!int.TryParse(InputPort, out int port) || port is < 0 or > 65535) {
|
|
||||||
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsToggleButtonEnabled = false;
|
|
||||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Starting);
|
|
||||||
ServerLauncher.Relaunch(port, InputToken, db);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StopServer() {
|
|
||||||
IsToggleButtonEnabled = false;
|
|
||||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Stopping);
|
|
||||||
ServerLauncher.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> OnClickToggleButton() {
|
|
||||||
if (ServerLauncher.IsRunning) {
|
|
||||||
StopServer();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return await StartServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnClickCopyTrackingScript() {
|
|
||||||
string bootstrap = await ResourceLoader.ReadTextAsync("Tracker/bootstrap.js");
|
|
||||||
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + ServerPort + ";")
|
|
||||||
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(ServerToken))
|
|
||||||
.Replace("/*[IMPORTS]*/", await ResourceLoader.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
|
||||||
.Replace("/*[CSS-CONTROLLER]*/", await ResourceLoader.ReadTextAsync("Tracker/styles/controller.css"))
|
|
||||||
.Replace("/*[CSS-SETTINGS]*/", await ResourceLoader.ReadTextAsync("Tracker/styles/settings.css"));
|
|
||||||
|
|
||||||
await Application.Current.Clipboard.SetTextAsync(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickRandomizeToken() {
|
|
||||||
InputToken = ServerUtils.GenerateRandomToken(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void OnClickApplyChanges() {
|
|
||||||
if (await StartServer()) {
|
|
||||||
ServerPort = InputPort;
|
|
||||||
ServerToken = InputToken;
|
|
||||||
OnPropertyChanged(nameof(HasMadeChanges));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickCancelChanges() {
|
|
||||||
InputPort = ServerPort;
|
|
||||||
InputToken = ServerToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
|
|
||||||
ServerStatusChanged?.Invoke(this, ServerLauncher.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
|
|
||||||
OnPropertyChanged(nameof(ToggleButtonText));
|
|
||||||
IsToggleButtonEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
|
|
||||||
Log.Error(ex);
|
|
||||||
|
|
||||||
string message = ex.Message;
|
|
||||||
Dispatcher.UIThread.Post(async () => { await Dialog.ShowOk(window, "Server Error", message); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:pages="clr-namespace:DHT.Desktop.Main.Pages"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="DHT.Desktop.Main.Pages.ViewerPage">
|
|
||||||
|
|
||||||
<Design.DataContext>
|
|
||||||
<pages:ViewerPageModel />
|
|
||||||
</Design.DataContext>
|
|
||||||
|
|
||||||
<UserControl.Styles>
|
|
||||||
<Style Selector="Grid > Label">
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Grid > CalendarDatePicker">
|
|
||||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
||||||
<Setter Property="IsTodayHighlighted" Value="True" />
|
|
||||||
<Setter Property="SelectedDateFormat" Value="Short" />
|
|
||||||
</Style>
|
|
||||||
</UserControl.Styles>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Vertical" Spacing="20">
|
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
|
|
||||||
<Button Command="{Binding OnClickOpenViewer}" Margin="0 0 5 0">Open Viewer</Button>
|
|
||||||
<Button Command="{Binding OnClickSaveViewer}" Margin="5 0 0 0">Save Viewer</Button>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Text="{Binding ExportedMessageText}" />
|
|
||||||
<StackPanel>
|
|
||||||
<CheckBox IsChecked="{Binding FilterByDate}">Filter by Date</CheckBox>
|
|
||||||
<Grid ColumnDefinitions="Auto, 4, 140" RowDefinitions="Auto, 4, Auto" Margin="4">
|
|
||||||
<Label Grid.Row="0" Grid.Column="0">From:</Label>
|
|
||||||
<CalendarDatePicker Grid.Row="0" Grid.Column="2" x:Name="StartDatePicker" IsEnabled="{Binding FilterByDate}" SelectedDateChanged="CalendarDatePicker_OnSelectedDateChanged" />
|
|
||||||
<Label Grid.Row="2" Grid.Column="0">To:</Label>
|
|
||||||
<CalendarDatePicker Grid.Row="2" Grid.Column="2" x:Name="EndDatePicker" IsEnabled="{Binding FilterByDate}" SelectedDateChanged="CalendarDatePicker_OnSelectedDateChanged" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</UserControl>
|
|
@@ -1,21 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages {
|
|
||||||
public class ViewerPage : UserControl {
|
|
||||||
public ViewerPage() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CalendarDatePicker_OnSelectedDateChanged(object? sender, SelectionChangedEventArgs e) {
|
|
||||||
if (DataContext is ViewerPageModel model) {
|
|
||||||
model.StartDate = this.FindControl<CalendarDatePicker>("StartDatePicker").SelectedDate;
|
|
||||||
model.EndDate = this.FindControl<CalendarDatePicker>("EndDatePicker").SelectedDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,128 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Web;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using DHT.Desktop.Models;
|
|
||||||
using DHT.Desktop.Resources;
|
|
||||||
using DHT.Server.Data.Filters;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Database.Export;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Pages {
|
|
||||||
public class ViewerPageModel : BaseModel {
|
|
||||||
public string ExportedMessageText { get; private set; } = "";
|
|
||||||
|
|
||||||
private bool filterByDate = false;
|
|
||||||
|
|
||||||
public bool FilterByDate {
|
|
||||||
get => filterByDate;
|
|
||||||
set => Change(ref filterByDate, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DateTime? startDate = null;
|
|
||||||
|
|
||||||
public DateTime? StartDate {
|
|
||||||
get => startDate;
|
|
||||||
set => Change(ref startDate, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DateTime? endDate = null;
|
|
||||||
|
|
||||||
public DateTime? EndDate {
|
|
||||||
get => endDate;
|
|
||||||
set => Change(ref endDate, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
private readonly IDatabaseFile db;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public ViewerPageModel() : this(null!, DummyDatabaseFile.Instance) {}
|
|
||||||
|
|
||||||
public ViewerPageModel(Window window, IDatabaseFile db) {
|
|
||||||
this.window = window;
|
|
||||||
this.db = db;
|
|
||||||
|
|
||||||
this.PropertyChanged += OnPropertyChanged;
|
|
||||||
this.db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
|
||||||
UpdateStatistics();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
|
||||||
if (e.PropertyName is nameof(FilterByDate) or nameof(StartDate) or nameof(EndDate)) {
|
|
||||||
UpdateStatistics();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
|
||||||
if (e.PropertyName == nameof(DatabaseStatistics.TotalMessages)) {
|
|
||||||
UpdateStatistics();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessageFilter CreateFilter() {
|
|
||||||
MessageFilter filter = new();
|
|
||||||
|
|
||||||
if (FilterByDate) {
|
|
||||||
filter.StartDate = StartDate;
|
|
||||||
filter.EndDate = EndDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateStatistics() {
|
|
||||||
ExportedMessageText = "Will export " + db.CountMessages(CreateFilter()) + " out of " + db.Statistics.TotalMessages + " message(s).";
|
|
||||||
OnPropertyChanged(nameof(ExportedMessageText));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> GenerateViewerContents() {
|
|
||||||
string json = ViewerJsonExport.Generate(db, CreateFilter());
|
|
||||||
|
|
||||||
string index = await ResourceLoader.ReadTextAsync("Viewer/index.html");
|
|
||||||
string viewer = index.Replace("/*[JS]*/", await ResourceLoader.ReadJoinedAsync("Viewer/scripts/", '\n'))
|
|
||||||
.Replace("/*[CSS]*/", await ResourceLoader.ReadJoinedAsync("Viewer/styles/", '\n'))
|
|
||||||
.Replace("/*[ARCHIVE]*/", HttpUtility.JavaScriptStringEncode(json));
|
|
||||||
return viewer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void OnClickOpenViewer() {
|
|
||||||
string rootPath = Path.Combine(Path.GetTempPath(), "DiscordHistoryTracker");
|
|
||||||
string filenameBase = Path.GetFileNameWithoutExtension(db.Path) + "-" + DateTime.Now.ToString("yyyy-MM-dd");
|
|
||||||
string fullPath = Path.Combine(rootPath, filenameBase + ".html");
|
|
||||||
int counter = 0;
|
|
||||||
|
|
||||||
while (File.Exists(fullPath)) {
|
|
||||||
fullPath = Path.Combine(rootPath, filenameBase + "-" + (++counter) + ".html");
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(rootPath);
|
|
||||||
await File.WriteAllTextAsync(fullPath, await GenerateViewerContents());
|
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void OnClickSaveViewer() {
|
|
||||||
var dialog = new SaveFileDialog {
|
|
||||||
Title = "Save Viewer",
|
|
||||||
InitialFileName = "archive.html",
|
|
||||||
Directory = Path.GetDirectoryName(db.Path),
|
|
||||||
Filters = new List<FileDialogFilter> {
|
|
||||||
new() {
|
|
||||||
Name = "Discord History Viewer",
|
|
||||||
Extensions = { "html" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.ShowAsync(window);
|
|
||||||
|
|
||||||
string path = await dialog;
|
|
||||||
if (!string.IsNullOrEmpty(path)) {
|
|
||||||
await File.WriteAllTextAsync(path, await GenerateViewerContents());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class WelcomeScreen : UserControl {
|
|
||||||
public WelcomeScreen() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent() {
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,91 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using DHT.Desktop.Dialogs;
|
|
||||||
using DHT.Desktop.Models;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Server.Database.Exceptions;
|
|
||||||
using DHT.Server.Database.Sqlite;
|
|
||||||
using DHT.Server.Logging;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Main {
|
|
||||||
public class WelcomeScreenModel : BaseModel {
|
|
||||||
public string Version => Program.Version;
|
|
||||||
|
|
||||||
public IDatabaseFile? Db { get; private set; }
|
|
||||||
public bool HasDatabase => Db != null;
|
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
|
|
||||||
private string? dbFilePath;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
|
||||||
public WelcomeScreenModel() : this(null!) {}
|
|
||||||
|
|
||||||
public WelcomeScreenModel(Window window) {
|
|
||||||
this.window = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void OpenOrCreateDatabase() {
|
|
||||||
var dialog = new SaveFileDialog {
|
|
||||||
Title = "Open or Create Database File",
|
|
||||||
InitialFileName = "archive.dht",
|
|
||||||
Directory = Path.GetDirectoryName(dbFilePath),
|
|
||||||
Filters = new List<FileDialogFilter> {
|
|
||||||
new() {
|
|
||||||
Name = "Discord History Tracker Database",
|
|
||||||
Extensions = { "dht" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.ShowAsync(window);
|
|
||||||
|
|
||||||
string path = await dialog;
|
|
||||||
if (!string.IsNullOrWhiteSpace(path)) {
|
|
||||||
await OpenOrCreateDatabaseFromPath(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OpenOrCreateDatabaseFromPath(string path) {
|
|
||||||
if (Db != null) {
|
|
||||||
Db = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbFilePath = path;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Db = await SqliteDatabaseFile.OpenOrCreate(path, CheckCanUpgradeDatabase);
|
|
||||||
} catch (InvalidDatabaseVersionException ex) {
|
|
||||||
await Dialog.ShowOk(window, "Database Error", "This database appears to be corrupted (invalid version: " + ex.Version + ").");
|
|
||||||
} catch (DatabaseTooNewException ex) {
|
|
||||||
await Dialog.ShowOk(window, "Database Error", "This database was opened in a newer version of DHT (database version " + ex.DatabaseVersion + ", app version " + ex.CurrentVersion + ").");
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.Error(ex);
|
|
||||||
await Dialog.ShowOk(window, "Database Error", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Db));
|
|
||||||
OnPropertyChanged(nameof(HasDatabase));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CheckCanUpgradeDatabase() {
|
|
||||||
return DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Database Upgrade", "This database was created with an older version of DHT. If you proceed, the database will be upgraded and will no longer open in previous versions of DHT. Do you want to proceed?");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseDatabase() {
|
|
||||||
Db = null;
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Db));
|
|
||||||
OnPropertyChanged(nameof(HasDatabase));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void ShowAboutDialog() {
|
|
||||||
await new AboutWindow() { DataContext = new AboutWindowModel() }.ShowDialog(this.window);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Exit() {
|
|
||||||
window.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Models {
|
|
||||||
public abstract class BaseModel : INotifyPropertyChanged {
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
[NotifyPropertyChangedInvocator]
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void Change<T>(ref T field, T newValue, [CallerMemberName] string? propertyName = null) {
|
|
||||||
if (!EqualityComparer<T>.Default.Equals(field, newValue)) {
|
|
||||||
field = newValue;
|
|
||||||
OnPropertyChanged(propertyName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,31 +1,40 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using DHT.Desktop.Resources;
|
||||||
|
|
||||||
namespace DHT.Desktop {
|
namespace DHT.Desktop;
|
||||||
internal static class Program {
|
|
||||||
public static string Version { get; }
|
|
||||||
|
|
||||||
static Program() {
|
static class Program {
|
||||||
Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "";
|
public static string Version { get; }
|
||||||
while (Version.EndsWith(".0")) {
|
public static CultureInfo Culture { get; }
|
||||||
Version = Version[..^2];
|
public static ResourceLoader Resources { get; }
|
||||||
}
|
|
||||||
|
|
||||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
static Program() {
|
||||||
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
Version = assembly.GetName().Version?.ToString() ?? "";
|
||||||
|
while (Version.EndsWith(".0")) {
|
||||||
|
Version = Version[..^2];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Main(string[] args) {
|
Culture = CultureInfo.CurrentCulture;
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||||
}
|
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
private static AppBuilder BuildAvaloniaApp() {
|
Resources = new ResourceLoader(assembly);
|
||||||
return AppBuilder.Configure<App>()
|
}
|
||||||
.UsePlatformDetect()
|
|
||||||
.LogToTrace();
|
public static void Main(string[] args) {
|
||||||
}
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppBuilder BuildAvaloniaApp() {
|
||||||
|
return AppBuilder.Configure<App.App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,41 +4,45 @@ using System.Reflection;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DHT.Desktop.Resources {
|
namespace DHT.Desktop.Resources;
|
||||||
public static class ResourceLoader {
|
|
||||||
private static Stream GetEmbeddedStream(string filename) {
|
public sealed class ResourceLoader {
|
||||||
Stream? stream = null;
|
private readonly Assembly assembly;
|
||||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
||||||
foreach (var embeddedName in assembly.GetManifestResourceNames()) {
|
public ResourceLoader(Assembly assembly) {
|
||||||
if (embeddedName.Replace('\\', '/') == filename) {
|
this.assembly = assembly;
|
||||||
stream = assembly.GetManifestResourceStream(embeddedName);
|
}
|
||||||
break;
|
|
||||||
}
|
private Stream GetEmbeddedStream(string filename) {
|
||||||
|
Stream? stream = null;
|
||||||
|
foreach (var embeddedName in assembly.GetManifestResourceNames()) {
|
||||||
|
if (embeddedName.Replace('\\', '/') == filename) {
|
||||||
|
stream = assembly.GetManifestResourceStream(embeddedName);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream ?? throw new ArgumentException("Missing embedded resource: " + filename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> ReadTextAsync(Stream stream) {
|
return stream ?? throw new ArgumentException("Missing embedded resource: " + filename);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
}
|
||||||
return await reader.ReadToEndAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> ReadTextAsync(string filename) {
|
private async Task<string> ReadTextAsync(Stream stream) {
|
||||||
return await ReadTextAsync(GetEmbeddedStream(filename));
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||||
}
|
return await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<string> ReadJoinedAsync(string path, char separator) {
|
public async Task<string> ReadTextAsync(string filename) {
|
||||||
StringBuilder joined = new();
|
return await ReadTextAsync(GetEmbeddedStream(filename));
|
||||||
|
}
|
||||||
|
|
||||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
public async Task<string> ReadJoinedAsync(string path, char separator) {
|
||||||
foreach (var embeddedName in assembly.GetManifestResourceNames()) {
|
StringBuilder joined = new();
|
||||||
if (embeddedName.Replace('\\', '/').StartsWith(path)) {
|
|
||||||
joined.Append(await ReadTextAsync(assembly.GetManifestResourceStream(embeddedName)!)).Append(separator);
|
foreach (var embeddedName in assembly.GetManifestResourceNames()) {
|
||||||
}
|
if (embeddedName.Replace('\\', '/').StartsWith(path)) {
|
||||||
|
joined.Append(await ReadTextAsync(assembly.GetManifestResourceStream(embeddedName)!)).Append(separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
return joined.ToString(0, Math.Max(0, joined.Length - 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return joined.ToString(0, Math.Max(0, joined.Length - 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
app/Desktop/Server/ServerManager.cs
Normal file
50
app/Desktop/Server/ServerManager.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Server;
|
||||||
|
|
||||||
|
sealed class ServerManager : IDisposable {
|
||||||
|
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
|
||||||
|
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
|
||||||
|
|
||||||
|
private static ServerManager? instance;
|
||||||
|
|
||||||
|
public bool IsRunning => ServerLauncher.IsRunning;
|
||||||
|
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
|
||||||
|
public ServerManager(IDatabaseFile db) {
|
||||||
|
if (db != DummyDatabaseFile.Instance) {
|
||||||
|
if (instance != null) {
|
||||||
|
throw new InvalidOperationException("Only one instance of ServerManager can exist at the same time!");
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Launch() {
|
||||||
|
ServerLauncher.Relaunch(Port, Token, db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Relaunch(ushort port, string token) {
|
||||||
|
Port = port;
|
||||||
|
Token = token;
|
||||||
|
Launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop() {
|
||||||
|
ServerLauncher.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
if (instance == this) {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
app/Directory.build.props
Normal file
8
app/Directory.build.props
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Project>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<LangVersion>11</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Desktop", "Desktop\Desktop.
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{7F94B470-B06F-43C0-9655-6592A9AE2D92}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{7F94B470-B06F-43C0-9655-6592A9AE2D92}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -18,5 +20,9 @@ Global
|
|||||||
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7F94B470-B06F-43C0-9655-6592A9AE2D92}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7EEC1C05-1D21-4B39-B1D6-CF51F3795629}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
19
app/Resources/Tracker/bootstrap.js
vendored
19
app/Resources/Tracker/bootstrap.js
vendored
@@ -42,6 +42,10 @@
|
|||||||
stopTrackingDelayed(() => isSending = false);
|
stopTrackingDelayed(() => isSending = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isNoAction = function(action) {
|
||||||
|
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||||
|
};
|
||||||
|
|
||||||
const onTrackingContinued = function(anyNewMessages) {
|
const onTrackingContinued = function(anyNewMessages) {
|
||||||
if (!STATE.isTracking()) {
|
if (!STATE.isTracking()) {
|
||||||
return;
|
return;
|
||||||
@@ -59,14 +63,14 @@
|
|||||||
if (SETTINGS.autoscroll) {
|
if (SETTINGS.autoscroll) {
|
||||||
let action = null;
|
let action = null;
|
||||||
|
|
||||||
if (!anyNewMessages) {
|
if (!DISCORD.hasMoreMessages()) {
|
||||||
action = SETTINGS.afterSavedMsg;
|
|
||||||
}
|
|
||||||
else if (!DISCORD.hasMoreMessages()) {
|
|
||||||
action = SETTINGS.afterFirstMsg;
|
action = SETTINGS.afterFirstMsg;
|
||||||
}
|
}
|
||||||
|
if (isNoAction(action) && !anyNewMessages) {
|
||||||
|
action = SETTINGS.afterSavedMsg;
|
||||||
|
}
|
||||||
|
|
||||||
if (action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING) {
|
if (isNoAction(action)) {
|
||||||
DISCORD.loadOlderMessages();
|
DISCORD.loadOlderMessages();
|
||||||
}
|
}
|
||||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||||
@@ -113,8 +117,8 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
DISCORD.loadOlderMessages();
|
|
||||||
isSending = false;
|
isSending = false;
|
||||||
|
onTrackingContinued(false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const anyNewMessages = await STATE.addDiscordMessages(info.id, messages);
|
const anyNewMessages = await STATE.addDiscordMessages(info.id, messages);
|
||||||
@@ -125,8 +129,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const callbackTimer = DISCORD.setupMessageCallback(onMessagesUpdated);
|
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||||
window.DHT_ON_UNLOAD.push(() => window.clearTimeout(callbackTimer));
|
|
||||||
|
|
||||||
STATE.onTrackingStateChanged(enabled => {
|
STATE.onTrackingStateChanged(enabled => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
class DOM{static id(id,parent){return(parent||document).getElementById(id)}static queryReactClass(cls,parent){return(parent||document).querySelector(`[class*="${cls}-"]`)}static createElement(tag,parent,id,html){const ele=document.createElement(tag);ele.id=id||"";ele.innerHTML=html||"";parent.appendChild(ele);return ele}static removeElement(ele){return ele.parentNode.removeChild(ele)}static createStyle(styles){return this.createElement("style",document.head,"",styles)}static saveToCookie(name,obj,expiresInSeconds){const expires=new Date(Date.now()+1e3*expiresInSeconds).toUTCString();document.cookie=name+"="+encodeURIComponent(JSON.stringify(obj))+";path=/;expires="+expires}static loadFromCookie(name){const value=document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)"+name+"\\s*\\=\\s*([^;]*).*$)|^.*$"),"$1");return value.length?JSON.parse(decodeURIComponent(value)):null}static getReactProps(ele){const keys=Object.keys(ele||{});let key=keys.find(key=>key.startsWith("__reactInternalInstance"));if(key){return ele[key].memoizedProps}key=keys.find(key=>key.startsWith("__reactProps$"));return key?ele[key]:null}}
|
|
@@ -1,17 +0,0 @@
|
|||||||
const GUI=function(){let controller=null;let settings=null;const stateChangedEvent=()=>{if(settings){settings.ui.cbAutoscroll.checked=SETTINGS.autoscroll;settings.ui.optsAfterFirstMsg[SETTINGS.afterFirstMsg].checked=true;settings.ui.optsAfterSavedMsg[SETTINGS.afterSavedMsg].checked=true;const autoscrollDisabled=!SETTINGS.autoscroll;Object.values(settings.ui.optsAfterFirstMsg).forEach(ele=>ele.disabled=autoscrollDisabled);Object.values(settings.ui.optsAfterSavedMsg).forEach(ele=>ele.disabled=autoscrollDisabled)}};return{showController(){if(controller){return}const html=`
|
|
||||||
<button id='dht-ctrl-close'>X</button>
|
|
||||||
<button id='dht-ctrl-settings'>Settings</button>
|
|
||||||
<button id='dht-ctrl-track'></button>
|
|
||||||
<p id='dht-ctrl-status'>Waiting</p>`;controller={styles:DOM.createStyle(`/*[CSS-CONTROLLER]*/`),ele:DOM.createElement("div",document.body,"dht-ctrl",html)};controller.ui={btnSettings:DOM.id("dht-ctrl-settings"),btnTrack:DOM.id("dht-ctrl-track"),btnClose:DOM.id("dht-ctrl-close"),textStatus:DOM.id("dht-ctrl-status")};controller.ui.btnSettings.addEventListener("click",()=>{this.showSettings()});controller.ui.btnTrack.addEventListener("click",()=>{const isTracking=!STATE.isTracking();STATE.setIsTracking(isTracking);if(!isTracking){controller.ui.textStatus.innerText="Stopped"}});controller.ui.btnClose.addEventListener("click",()=>{this.hideController();window.DHT_ON_UNLOAD.forEach(f=>f());window.DHT_LOADED=false});STATE.onTrackingStateChanged(isTracking=>{controller.ui.btnTrack.innerText=isTracking?"Pause Tracking":"Start Tracking";controller.ui.btnSettings.disabled=isTracking});SETTINGS.onSettingsChanged(stateChangedEvent);stateChangedEvent()},hideController(){if(controller){DOM.removeElement(controller.ele);DOM.removeElement(controller.styles);controller=null}},showSettings(){if(settings){return}const radio=(type,id,label)=>"<label><input id='dht-cfg-"+type+"-"+id+"' name='dht-"+type+"' type='radio'> "+label+"</label><br>";const html=`
|
|
||||||
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
|
|
||||||
<br>
|
|
||||||
<label>After reaching the first message in channel...</label><br>
|
|
||||||
${radio("afm","nothing","Do Nothing")}
|
|
||||||
${radio("afm","pause","Pause Tracking")}
|
|
||||||
${radio("afm","switch","Switch to Next Channel")}
|
|
||||||
<br>
|
|
||||||
<label>After reaching a previously saved message...</label><br>
|
|
||||||
${radio("asm","nothing","Do Nothing")}
|
|
||||||
${radio("asm","pause","Pause Tracking")}
|
|
||||||
${radio("asm","switch","Switch to Next Channel")}
|
|
||||||
<p id='dht-cfg-note'>It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.</p>`;settings={styles:DOM.createStyle(`/*[CSS-SETTINGS]*/`),overlay:DOM.createElement("div",document.body,"dht-cfg-overlay"),ele:DOM.createElement("div",document.body,"dht-cfg",html)};settings.overlay.addEventListener("click",()=>{this.hideSettings()});settings.ui={cbAutoscroll:DOM.id("dht-cfg-autoscroll"),optsAfterFirstMsg:{},optsAfterSavedMsg:{}};settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING]=DOM.id("dht-cfg-afm-nothing");settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE]=DOM.id("dht-cfg-afm-pause");settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH]=DOM.id("dht-cfg-afm-switch");settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING]=DOM.id("dht-cfg-asm-nothing");settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE]=DOM.id("dht-cfg-asm-pause");settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH]=DOM.id("dht-cfg-asm-switch");settings.ui.cbAutoscroll.addEventListener("change",()=>{SETTINGS.autoscroll=settings.ui.cbAutoscroll.checked});Object.keys(settings.ui.optsAfterFirstMsg).forEach(key=>{settings.ui.optsAfterFirstMsg[key].addEventListener("click",()=>{SETTINGS.afterFirstMsg=key})});Object.keys(settings.ui.optsAfterSavedMsg).forEach(key=>{settings.ui.optsAfterSavedMsg[key].addEventListener("click",()=>{SETTINGS.afterSavedMsg=key})});stateChangedEvent()},hideSettings(){if(settings){DOM.removeElement(settings.overlay);DOM.removeElement(settings.ele);DOM.removeElement(settings.styles);settings=null}},setStatus(state){if(controller){controller.ui.textStatus.innerText=state}}}}();
|
|
@@ -1 +0,0 @@
|
|||||||
const CONSTANTS={AUTOSCROLL_ACTION_NOTHING:"optNothing",AUTOSCROLL_ACTION_PAUSE:"optPause",AUTOSCROLL_ACTION_SWITCH:"optSwitch"};let IS_FIRST_RUN=false;const SETTINGS=function(){const settingsChangedEvents=[];const saveSettings=function(){DOM.saveToCookie("DHT_SETTINGS",root,60*60*24*365*5)};const triggerSettingsChanged=function(property){for(const callback of settingsChangedEvents){callback(property)}saveSettings()};const defineTriggeringProperty=function(obj,property,value){const name="_"+property;Object.defineProperty(obj,property,{get:()=>obj[name],set:value=>{obj[name]=value;triggerSettingsChanged(property)}});obj[name]=value};let loaded=DOM.loadFromCookie("DHT_SETTINGS");if(!loaded){loaded={_autoscroll:true,_afterFirstMsg:CONSTANTS.AUTOSCROLL_ACTION_PAUSE,_afterSavedMsg:CONSTANTS.AUTOSCROLL_ACTION_PAUSE};IS_FIRST_RUN=true}const root={onSettingsChanged(callback){settingsChangedEvents.push(callback)}};defineTriggeringProperty(root,"autoscroll",loaded._autoscroll);defineTriggeringProperty(root,"afterFirstMsg",loaded._afterFirstMsg);defineTriggeringProperty(root,"afterSavedMsg",loaded._afterSavedMsg);if(IS_FIRST_RUN){saveSettings()}return root}();
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user