mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-08-17 19:31:42 +02:00
Compare commits
331 Commits
master-bro
...
5866d953f8
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5866d953f8 | ||
16509be56c
|
|||
791171a79b
|
|||
08dec7deb3
|
|||
b148a5634f
|
|||
57e2f9ed80
|
|||
895d69279f
|
|||
0176fa5d53
|
|||
b5a5826cf9
|
|||
![]() |
6c0e0ff697 | ||
45593cb140
|
|||
c770a066c4
|
|||
43bf1fcafe
|
|||
8857f6c3b1
|
|||
87de0b98b5
|
|||
295370a3d4
|
|||
312be6609d
|
|||
44b42657ef
|
|||
![]() |
7e29c7f837 | ||
0d3600492e
|
|||
3b569ad5d6
|
|||
![]() |
7ba012ef5c | ||
![]() |
c52572b387 | ||
![]() |
5ba80dc12f | ||
![]() |
37a0feddcc | ||
![]() |
47f448dcde | ||
![]() |
0281b49815 | ||
![]() |
648b221bb8 | ||
![]() |
7d8558ae04 | ||
![]() |
41053549ab | ||
![]() |
f279bb4d16 | ||
b2276600c7
|
|||
40269f591b
|
|||
dea3b272c0
|
|||
e66206b4a8
|
|||
780d5ae421
|
|||
38f79dee7d
|
|||
e30b305eb5
|
|||
fa17d0e224
|
|||
5741fad528
|
|||
e7c52fd1d5
|
|||
fb9b31175c
|
|||
f8e56ae367
|
|||
7ed31fbc8b
|
|||
e92aeb5a1b
|
|||
50d5bdba97
|
|||
295c32c7b4
|
|||
5e4bb5b44d
|
|||
96c19afa66
|
|||
a45f60b528
|
|||
62b97807b8
|
|||
0fff3e8eaf
|
|||
02b773db8f
|
|||
261a583b50
|
|||
2ecb658e42
|
|||
7a6dd23912
|
|||
60761d80ab
|
|||
e11db62015
|
|||
bfac9b91d9
|
|||
4215dc76f4
|
|||
86adda610b
|
|||
f1c4cd040e
|
|||
737ab43ea4
|
|||
0229c00259
|
|||
95361d2e01
|
|||
5b7312109b
|
|||
9f1db9a662
|
|||
d3e1bb901c
|
|||
9bd105f57e
|
|||
e1eae393c3
|
|||
fbed74529d
|
|||
8924aa7c06
|
|||
9738880ba7
|
|||
1700f99bf7
|
|||
84acf5f5d5
|
|||
ae56433836
|
|||
c3d4fa5532
|
|||
a6225b9721
|
|||
943163473a
|
|||
fa00df10d8
|
|||
f54465e5fe
|
|||
2d55ca4013
|
|||
0072e025f4
|
|||
383ee5c90e
|
|||
2b1c178f49
|
|||
16d7e5a6f8
|
|||
052d24d1e7
|
|||
b058f1fb77
|
|||
d4da64a5ed
|
|||
8de309a6c4
|
|||
de8d6a1e11
|
|||
d79e6f53b4
|
|||
70c04fc986
|
|||
c8d8d95daa
|
|||
daafdbbfaf
|
|||
07615de87a
|
|||
7fdc19880e
|
|||
67b9c12843
|
|||
9030a2f010
|
|||
a6dad6b4c7
|
|||
72b8fb7c14
|
|||
7173dc6cfc
|
|||
2c1e5a7603
|
|||
4929a19397
|
|||
c5f77872fe
|
|||
c9e50e1a80
|
|||
ef3e34066a
|
|||
37374eeb18
|
|||
23ddb45a0d
|
|||
9904a711f7
|
|||
d5720c8758
|
|||
89161e14b1
|
|||
9d208b026c
|
|||
119649ef9b
|
|||
0bc6232da7
|
|||
de266473c5
|
|||
e0f359c15b
|
|||
935f11d736
|
|||
f64141e768
|
|||
edea3470df
|
|||
031d521402
|
|||
0131f8cb50
|
|||
3bf5acfa65
|
|||
f603c861c5
|
|||
d2934f4d6a
|
|||
567253d147
|
|||
aa6555990c
|
|||
3d9d6a454a
|
|||
ee39780928
|
|||
7b58f973a0
|
|||
93fe018343
|
|||
4f5e27f651
|
|||
cbf81ec95a
|
|||
8a80cb8c20
|
|||
865deb356a
|
|||
069ab97196
|
|||
caab038eaa
|
|||
fb837374fc
|
|||
65d935cca1
|
|||
6e64c86d7a
|
|||
8aeb590bb3
|
|||
8dc1adc9f0
|
|||
ddf70b02e7
|
|||
ef59fd992e
|
|||
d044627fac
|
|||
a624745602 | |||
6da3c185e5 | |||
d4d14cab97
|
|||
095c9a061a
|
|||
d01f9ed218
|
|||
dd6f121059
|
|||
8bba33d815
|
|||
9eab8ac92a
|
|||
fe588686fc
|
|||
7392987165
|
|||
492dddb35d
|
|||
d35280a6a6
|
|||
ae8a34f938
|
|||
18f5823f2a
|
|||
37640c97b0
|
|||
3cc5c75c48
|
|||
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
|
|||
52998f7d7a
|
|||
c2e302f8bb
|
|||
9f506aceb2
|
|||
bbb50dc50c
|
|||
3f763bbf6b
|
|||
be3a7d6d80
|
|||
bd78051a9d
|
|||
6a24befca1
|
|||
09dce7b062
|
|||
396c622d9c
|
|||
73bf16a21e
|
|||
25071d4323
|
|||
ab7b3532fc
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1 @@
|
|||||||
github: chylex
|
|
||||||
patreon: chylex
|
|
||||||
ko_fi: chylex
|
ko_fi: chylex
|
||||||
|
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,5 +1,9 @@
|
|||||||
/inspectionProfiles/
|
/codeStyles
|
||||||
|
/dataSources.local.xml
|
||||||
|
/deployment.xml
|
||||||
/httpRequests/
|
/httpRequests/
|
||||||
/shelf/
|
/inspectionProfiles
|
||||||
/misc.xml
|
/misc.xml
|
||||||
|
/shelf/
|
||||||
|
/webServers.xml
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
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" />
|
||||||
|
4
.idea/php.xml
generated
Normal file
4
.idea/php.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PhpProjectSharedConfiguration" php_language_level="7.4" />
|
||||||
|
</project>
|
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": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
51
README.md
51
README.md
@@ -1,48 +1,45 @@
|
|||||||
# 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
|
|
||||||
* `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 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.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 Debian in WSL and open a terminal in the project folder.
|
||||||
|
2. Run the `app/build.wsl.sh` script.
|
||||||
|
3. Read the [Distribution](#distribution) section below.
|
||||||
|
|
||||||
The build process automatically minifies JS using `UglifyJS@3`, and CSS using a custom minifier.
|
Note: The build script expects `dotnet.exe` to be installed in `C:\Program Files\dotnet`.
|
||||||
|
|
||||||
* If the `--nominify` flag is used, minification will be completely disabled
|
#### Release – Other Operating Systems
|
||||||
* 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
|
1. Install the `zip` package from your repository.
|
||||||
* 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/`
|
2. Run the `app/build.sh` script.
|
||||||
* When building on Linux/Mac, the build script will attempt to find `uglifyjs` in the command line
|
3. 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 the ASP.NET Core Runtime to be installed.
|
||||||
|
2
app/.editorconfig
Normal file
2
app/.editorconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[*.cs]
|
||||||
|
propertychanged.auto_notify = false
|
7
app/.gitignore
vendored
Normal file
7
app/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.vscode/
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
*.user
|
11
app/.idea/.idea.DiscordHistoryTracker/.idea/.gitignore
generated
vendored
Normal file
11
app/.idea/.idea.DiscordHistoryTracker/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/.idea.DiscordHistoryTracker.iml
|
||||||
|
/contentModel.xml
|
||||||
|
/dataSources
|
||||||
|
/dataSources.local.xml
|
||||||
|
/dictionaries
|
||||||
|
/httpRequests/
|
||||||
|
/misc.xml
|
||||||
|
/modules.xml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
1
app/.idea/.idea.DiscordHistoryTracker/.idea/.name
generated
Normal file
1
app/.idea/.idea.DiscordHistoryTracker/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DiscordHistoryTracker
|
29
app/.idea/.idea.DiscordHistoryTracker/.idea/avalonia.xml
generated
Normal file
29
app/.idea/.idea.DiscordHistoryTracker/.idea/avalonia.xml
generated
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?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/CheckBox/CheckBoxDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Dialogs/Message/MessageDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Dialogs/Progress/ProgressDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Dialogs/TextBox/TextBoxDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/AboutWindow.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Controls/DownloadItemFilterPanel.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Controls/MessageFilterPanel.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Controls/ServerConfigurationPanel.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Controls/StatusBar.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Dialogs/NewDatabaseSettingsDialog.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/MainWindow.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Pages/AdvancedPage.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Pages/DatabasePage.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Pages/DebugPage.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Pages/DownloadsPage.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/Screens/MainContentScreen.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
<entry key="Desktop/Main/Screens/WelcomeScreen.axaml" value="Desktop/Desktop.csproj" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
457
app/.idea/.idea.DiscordHistoryTracker/.idea/codeStyles/Project.xml
generated
Normal file
457
app/.idea/.idea.DiscordHistoryTracker/.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="AUTODETECT_INDENTS" value="false" />
|
||||||
|
<option name="OTHER_INDENT_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="LINE_SEPARATOR" value=" " />
|
||||||
|
<option name="RIGHT_MARGIN" value="999" />
|
||||||
|
<option name="FORMATTER_TAGS_ENABLED" value="true" />
|
||||||
|
<CssCodeStyleSettings>
|
||||||
|
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
||||||
|
</CssCodeStyleSettings>
|
||||||
|
<DB2CodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</DB2CodeStyleSettings>
|
||||||
|
<DerbyCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</DerbyCodeStyleSettings>
|
||||||
|
<GoCodeStyleSettings>
|
||||||
|
<option name="MOVE_ALL_STDLIB_IMPORTS_IN_ONE_GROUP" value="true" />
|
||||||
|
<option name="GROUP_STDLIB_IMPORTS" value="true" />
|
||||||
|
<option name="WRAP_COMP_LIT" value="5" />
|
||||||
|
<option name="WRAP_FUNC_PARAMS" value="5" />
|
||||||
|
<option name="WRAP_FUNC_RESULT" value="5" />
|
||||||
|
</GoCodeStyleSettings>
|
||||||
|
<H2CodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</H2CodeStyleSettings>
|
||||||
|
<HSQLCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</HSQLCodeStyleSettings>
|
||||||
|
<HTMLCodeStyleSettings>
|
||||||
|
<option name="HTML_ALIGN_TEXT" value="true" />
|
||||||
|
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
<option name="HTML_ELEMENTS_TO_REMOVE_NEW_LINE_BEFORE" value="" />
|
||||||
|
<option name="HTML_DO_NOT_INDENT_CHILDREN_OF" value="" />
|
||||||
|
</HTMLCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACKETS" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
<option name="USE_CHAINED_CALLS_GROUP_INDENTS" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_ASYNC_ARROW_LPAREN" value="false" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<JSON>
|
||||||
|
<option name="OBJECT_WRAPPING" value="5" />
|
||||||
|
<option name="ARRAY_WRAPPING" value="5" />
|
||||||
|
</JSON>
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
|
||||||
|
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||||
|
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||||
|
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value>
|
||||||
|
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="ALIGN_IN_COLUMNS_CASE_BRANCH" value="true" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
|
<option name="IMPORT_NESTED_CLASSES" value="true" />
|
||||||
|
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
|
||||||
|
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<LessCodeStyleSettings>
|
||||||
|
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
||||||
|
</LessCodeStyleSettings>
|
||||||
|
<MSSQLCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</MSSQLCodeStyleSettings>
|
||||||
|
<MySQLCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</MySQLCodeStyleSettings>
|
||||||
|
<Objective-C>
|
||||||
|
<option name="INDENT_DIRECTIVE_AS_CODE" value="true" />
|
||||||
|
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_CASE_EXPRESSIONS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_INIT_LIST" value="true" />
|
||||||
|
<option name="SPACE_AFTER_DICTIONARY_LITERAL_COLON" value="false" />
|
||||||
|
</Objective-C>
|
||||||
|
<OracleCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</OracleCodeStyleSettings>
|
||||||
|
<PHPCodeStyleSettings>
|
||||||
|
<option name="ALIGN_KEY_VALUE_PAIRS" value="true" />
|
||||||
|
<option name="CONCAT_SPACES" value="false" />
|
||||||
|
<option name="COMMA_AFTER_LAST_ARRAY_ELEMENT" value="true" />
|
||||||
|
<option name="PHPDOC_BLANK_LINE_BEFORE_TAGS" value="true" />
|
||||||
|
<option name="LOWER_CASE_BOOLEAN_CONST" value="true" />
|
||||||
|
<option name="LOWER_CASE_NULL_CONST" value="true" />
|
||||||
|
<option name="ELSE_IF_STYLE" value="COMBINE" />
|
||||||
|
<option name="VARIABLE_NAMING_STYLE" value="SNAKE_CASE" />
|
||||||
|
<option name="KEEP_BLANK_LINES_AFTER_LBRACE" value="0" />
|
||||||
|
<option name="SPACE_BEFORE_CLOSURE_LEFT_PARENTHESIS" value="false" />
|
||||||
|
<option name="FORCE_SHORT_DECLARATION_ARRAY_STYLE" value="true" />
|
||||||
|
<option name="NEW_LINE_AFTER_PHP_OPENING_TAG" value="true" />
|
||||||
|
<option name="SPACE_AROUND_ASSIGNMENT_IN_DECLARE" value="true" />
|
||||||
|
</PHPCodeStyleSettings>
|
||||||
|
<PostgresCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</PostgresCodeStyleSettings>
|
||||||
|
<Properties>
|
||||||
|
<option name="KEEP_BLANK_LINES" value="true" />
|
||||||
|
</Properties>
|
||||||
|
<Python>
|
||||||
|
<option name="SPACE_AROUND_EQ_IN_NAMED_PARAMETER" value="true" />
|
||||||
|
<option name="SPACE_AROUND_EQ_IN_KEYWORD_ARGUMENT" value="true" />
|
||||||
|
<option name="NEW_LINE_AFTER_COLON" value="true" />
|
||||||
|
<option name="DICT_WRAPPING" value="5" />
|
||||||
|
<option name="DICT_NEW_LINE_AFTER_LEFT_BRACE" value="true" />
|
||||||
|
<option name="DICT_NEW_LINE_BEFORE_RIGHT_BRACE" value="true" />
|
||||||
|
</Python>
|
||||||
|
<RsCodeStyleSettings>
|
||||||
|
<option name="ALIGN_RET_TYPE" value="false" />
|
||||||
|
<option name="ALIGN_TYPE_PARAMS" value="true" />
|
||||||
|
<option name="ALLOW_ONE_LINE_MATCH" value="true" />
|
||||||
|
<option name="SPACE_AROUND_ASSOC_TYPE_BINDING" value="true" />
|
||||||
|
</RsCodeStyleSettings>
|
||||||
|
<Ruby>
|
||||||
|
<option name="INDENT_PRIVATE_METHODS" value="true" />
|
||||||
|
<option name="INDENT_PROTECTED_METHODS" value="true" />
|
||||||
|
<option name="INDENT_PUBLIC_METHODS" value="true" />
|
||||||
|
<option name="INDENT_WHEN_CASES" value="true" />
|
||||||
|
<option name="CHAIN_CALLS_ALIGNMENT" value="2" />
|
||||||
|
</Ruby>
|
||||||
|
<SQLiteCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</SQLiteCodeStyleSettings>
|
||||||
|
<ScssCodeStyleSettings>
|
||||||
|
<option name="HEX_COLOR_LOWER_CASE" value="true" />
|
||||||
|
</ScssCodeStyleSettings>
|
||||||
|
<Shell_Script>
|
||||||
|
<option name="KEEP_COLUMN_ALIGNMENT_PADDING" value="true" />
|
||||||
|
</Shell_Script>
|
||||||
|
<SqlCodeStyleSettings version="7">
|
||||||
|
<option name="KEYWORD_CASE" value="2" />
|
||||||
|
<option name="TYPE_CASE" value="2" />
|
||||||
|
<option name="CUSTOM_TYPE_CASE" value="2" />
|
||||||
|
<option name="SUBQUERY_CONTENT" value="1" />
|
||||||
|
<option name="SUBQUERY_CLOSING" value="1" />
|
||||||
|
<option name="INSERT_TABLE_EL_LINE" value="0" />
|
||||||
|
<option name="INSERT_EL_WRAP" value="2" />
|
||||||
|
<option name="SET_EL_WRAP" value="2" />
|
||||||
|
<option name="SET_ALIGN_EQUAL_SIGN" value="false" />
|
||||||
|
<option name="FROM_EL_WRAP" value="2" />
|
||||||
|
<option name="FROM_ALIGN_JOIN_TABLES" value="true" />
|
||||||
|
<option name="FROM_INDENT_JOIN" value="false" />
|
||||||
|
<option name="FROM_ONLY_JOIN_INDENT" value="2" />
|
||||||
|
<option name="WHERE_EL_WRAP" value="2" />
|
||||||
|
<option name="TABLE_OPENING" value="1" />
|
||||||
|
<option name="TABLE_CONTENT" value="2" />
|
||||||
|
<option name="TABLE_CLOSING" value="3" />
|
||||||
|
<option name="TABLE_DEFAULTS_ALIGN" value="false" />
|
||||||
|
<option name="TABLE_NULLABILITIES_ALIGN" value="false" />
|
||||||
|
<option name="CONSTRAINT_WRAP_1" value="false" />
|
||||||
|
<option name="CONSTRAINT_WRAP_3" value="true" />
|
||||||
|
<option name="CONSTRAINT_WRAP_4" value="true" />
|
||||||
|
<option name="VIEW_INDENT_QUERY" value="true" />
|
||||||
|
<option name="IMP_LOOP_LOOP_INDENT" value="false" />
|
||||||
|
<option name="EXPR_CASE_WHEN_WRAP" value="false" />
|
||||||
|
</SqlCodeStyleSettings>
|
||||||
|
<SybaseCodeStyleSettings version="7">
|
||||||
|
<option name="USE_GENERIC_STYLE" value="true" />
|
||||||
|
</SybaseCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
<option name="USE_CHAINED_CALLS_GROUP_INDENTS" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_ASYNC_ARROW_LPAREN" value="false" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<XML>
|
||||||
|
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
</XML>
|
||||||
|
<codeStyleSettings language="CMake">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="CSS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Gherkin">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Groovy">
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JAVA">
|
||||||
|
<option name="RIGHT_MARGIN" value="999" />
|
||||||
|
<option name="BLANK_LINES_AFTER_PACKAGE" value="0" />
|
||||||
|
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
|
||||||
|
<option name="SPACE_AFTER_TYPE_CAST" value="false" />
|
||||||
|
<option name="SPACE_BEFORE_SYNCHRONIZED_PARENTHESES" value="false" />
|
||||||
|
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JSON">
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="SPACE_WITHIN_BRACKETS" value="true" />
|
||||||
|
<option name="SPACE_WITHIN_BRACES" value="true" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="LESS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Lua">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Markdown">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="ObjectiveC">
|
||||||
|
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="PHP">
|
||||||
|
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
|
||||||
|
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||||
|
<option name="CLASS_BRACE_STYLE" value="1" />
|
||||||
|
<option name="METHOD_BRACE_STYLE" value="1" />
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="SPECIAL_ELSE_IF_TREATMENT" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Puppet">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Python">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="RHTML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Rust">
|
||||||
|
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="SASS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="SCSS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="SQL">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TOML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
|
||||||
|
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="go">
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="5" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||||
|
<option name="ELSE_ON_NEW_LINE" value="true" />
|
||||||
|
<option name="METHOD_ANNOTATION_WRAP" value="0" />
|
||||||
|
<option name="CLASS_ANNOTATION_WRAP" value="0" />
|
||||||
|
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="liquid">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="ruby">
|
||||||
|
<option name="SPACE_WITHIN_BRACES" value="true" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="yaml">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
5
app/.idea/.idea.DiscordHistoryTracker/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
app/.idea/.idea.DiscordHistoryTracker/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
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>
|
6
app/.idea/.idea.DiscordHistoryTracker/.idea/encodings.xml
generated
Normal file
6
app/.idea/.idea.DiscordHistoryTracker/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||||
|
<file url="PROJECT" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
app/.idea/.idea.DiscordHistoryTracker/.idea/indexLayout.xml
generated
Normal file
10
app/.idea/.idea.DiscordHistoryTracker/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders>
|
||||||
|
<Path>Resources</Path>
|
||||||
|
</attachedFolders>
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
849
app/.idea/.idea.DiscordHistoryTracker/.idea/inspectionProfiles/Project.xml
generated
Normal file
849
app/.idea/.idea.DiscordHistoryTracker/.idea/inspectionProfiles/Project.xml
generated
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project" />
|
||||||
|
<inspection_tool class="AbstractMethodCallInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AccessToNonThreadSafeStaticFieldFromInstance" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="nonThreadSafeClasses">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="nonThreadSafeTypes" value="" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AccessToStaticFieldLockedOnInstance" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AddOperatorModifier" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AmbiguousFieldAccess" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AmbiguousMethodCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AnonymousInnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ArrayEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssertEqualsCalledOnArray" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssertsWithoutMessagesTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentOrReturnOfFieldWithMutableType" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentToCatchBlockParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentToMethodParameter" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreTransformationOfOriginalParameter" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AssignmentToStaticFieldFromInstanceMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AutoBoxing" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreAddedToCollection" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredTypes" value="java.util.stream.Stream,java.util.stream.IntStream,java.util.stream.LongStream,java.util.stream.DoubleStream,net.minecraft.client.Minecraft,net.minecraft.client.MainWindow" />
|
||||||
|
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,net.minecraft.client.MinecraftClient,getInstance|getWindow" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AutoUnboxing" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AwaitNotInLoop" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AwaitWithoutCorrespondingSignal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BadOddness" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BigDecimalEquals" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BigDecimalLegacyMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BooleanExpressionMayBeConditional" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CallToNativeMethodWhileLocked" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CallToSimpleGetterInClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreGetterCallsOnOtherObjects" value="false" />
|
||||||
|
<option name="onlyReportPrivateGetter" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="CallToSimpleSetterInClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSetterCallsOnOtherObjects" value="false" />
|
||||||
|
<option name="onlyReportPrivateSetter" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CallableParameterUseCaseInTypeContextInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="CascadeStringReplacementInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="USE_SHORT_ARRAYS_SYNTAX" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CastToIncompatibleInterface" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ChannelResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ClassIndependentOfModule" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassLoaderInstantiation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassMayBeInterface" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="reportClassesWithNonAbstractMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ClassMethodNameMatchesFieldNameInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassNestingDepth" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_limit" value="1" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ClassNewInstance" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassOnlyUsedInOneModule" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassOnlyUsedInOnePackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassOverridesFieldOfSuperClassInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="REPORT_PRIVATE_REDEFINITION" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ClassReferencesSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassUnconnectedToPackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CloneCallsConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CloneInNonCloneableClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CloneReturnsClassType" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CloneableClassInSecureContext" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CloneableImplementsClone" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreCloneableDueToInheritance" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="CollectionContainsUrl" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CollectionsFieldAccessReplaceableByMethodCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ComparableImplementedButEqualsNotOverridden" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ComparatorNotSerializable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CompareToUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ComparisonOfShortAndChar" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConditionSignal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConditionalExpression" enabled="true" level="INFORMATION" enabled_by_default="true">
|
||||||
|
<option name="ignoreSimpleAssignmentsAndReturns" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConfusingElse" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||||
|
<option name="reportWhenNoStatementFollow" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ConfusingMainMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConfusingOctalEscape" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConfusingPlusesOrMinusesJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstantJUnitAssertArgument" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstantMathCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstantTestNGAssertArgument" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstructorCount" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreDeprecatedConstructors" value="true" />
|
||||||
|
<option name="m_limit" value="5" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConvertJavadoc" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConvertLambdaToReference" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConvertOldAnnotations" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CssConvertColorToHexInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CssConvertColorToRgbInspection" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CssMissingSemicolon" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CssReplaceWithShorthandUnsafely" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CustomClassloader" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CustomSecurityManager" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CyclicClassDependency" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CyclicPackageDependency" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DateToString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DebuggerStatementJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DeclareCollectionAsInterface" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreLocalVariables" value="false" />
|
||||||
|
<option name="ignorePrivateMethodsAndFields" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DisallowWritingIntoStaticPropertiesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DisconnectedForeachInstructionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="DisjointPackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DivideByZeroJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DocumentWriteJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DoubleBraceInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DoubleCheckedLocking" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreOnVolatileVariables" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="DriverManagerGetConnection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DuplicateConditionJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DynamicallyGeneratedCodeJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ES6ConvertIndexedForToForOf" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ES6ConvertLetToConst" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ES6ConvertToForOf" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ES6ShorthandObjectProperty" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ES6TopLevelAwaitExpression" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EmptyDirectory" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EmptyStatementBody" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_reportEmptyBlocks" value="true" />
|
||||||
|
<option name="commentsAreContent" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="EmptySynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EnumSwitchStatementWhichMissesCases" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSwitchStatementsWithDefault" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="EnumerationCanBeIteration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualsCalledOnEnumConstant" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualsHashCodeCalledOnUrl" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualsUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ErrorRethrown" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExceptionNameDoesntEndWithException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExceptionPackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExpectedExceptionNeverThrownTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExplicitArgumentCanBeLambda" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExtendsThread" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExtendsThrowable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExternalizableWithSerializationMethods" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FallthruInSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FieldAccessedSynchronizedAndUnsynchronized" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="countGettersAndSetters" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FieldDeclarationSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FieldHidesSuperclassField" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreInvisibleFields" value="false" />
|
||||||
|
<option name="ignoreStaticFields" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FieldMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="Finalize" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreTrivialFinalizers" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FinalizeNotProtected" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FixedTimeStartWithInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FloatingPointEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FoldInitializerAndIfToElvis" enabled="false" level="INFO" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ForLoopThatDoesntUseLoopVariableJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ForgottenDebugOutputInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="configuration">
|
||||||
|
<list>
|
||||||
|
<option value="\Codeception\Util\Debug::debug" />
|
||||||
|
<option value="\Codeception\Util\Debug::pause" />
|
||||||
|
<option value="\Doctrine\Common\Util\Debug::dump" />
|
||||||
|
<option value="\Doctrine\Common\Util\Debug::export" />
|
||||||
|
<option value="\Illuminate\Support\Debug\Dumper::dump" />
|
||||||
|
<option value="\Symfony\Component\Debug\Debug::enable" />
|
||||||
|
<option value="\Symfony\Component\Debug\DebugClassLoader::enable" />
|
||||||
|
<option value="\Symfony\Component\Debug\ErrorHandler::register" />
|
||||||
|
<option value="\Symfony\Component\Debug\ExceptionHandler::register" />
|
||||||
|
<option value="\TYPO3\CMS\Core\Utility\DebugUtility::debug" />
|
||||||
|
<option value="\Zend\Debug\Debug::dump" />
|
||||||
|
<option value="\Zend\Di\Display\Console::export" />
|
||||||
|
<option value="dd" />
|
||||||
|
<option value="debug_print_backtrace" />
|
||||||
|
<option value="debug_zval_dump" />
|
||||||
|
<option value="dpm" />
|
||||||
|
<option value="dpq" />
|
||||||
|
<option value="dsm" />
|
||||||
|
<option value="dump" />
|
||||||
|
<option value="dvm" />
|
||||||
|
<option value="error_log" />
|
||||||
|
<option value="kpr" />
|
||||||
|
<option value="phpinfo" />
|
||||||
|
<option value="print_r" />
|
||||||
|
<option value="var_dump" />
|
||||||
|
<option value="var_export" />
|
||||||
|
<option value="wp_die" />
|
||||||
|
<option value="xdebug_break" />
|
||||||
|
<option value="xdebug_call_class" />
|
||||||
|
<option value="xdebug_call_file" />
|
||||||
|
<option value="xdebug_call_function" />
|
||||||
|
<option value="xdebug_call_line" />
|
||||||
|
<option value="xdebug_code_coverage_started" />
|
||||||
|
<option value="xdebug_debug_zval" />
|
||||||
|
<option value="xdebug_debug_zval_stdout" />
|
||||||
|
<option value="xdebug_dump_superglobals" />
|
||||||
|
<option value="xdebug_enable" />
|
||||||
|
<option value="xdebug_get_code_coverage" />
|
||||||
|
<option value="xdebug_get_collected_errors" />
|
||||||
|
<option value="xdebug_get_declared_vars" />
|
||||||
|
<option value="xdebug_get_function_stack" />
|
||||||
|
<option value="xdebug_get_headers" />
|
||||||
|
<option value="xdebug_get_monitored_functions" />
|
||||||
|
<option value="xdebug_get_profiler_filename" />
|
||||||
|
<option value="xdebug_get_stack_depth" />
|
||||||
|
<option value="xdebug_get_tracefile_name" />
|
||||||
|
<option value="xdebug_is_enabled" />
|
||||||
|
<option value="xdebug_memory_usage" />
|
||||||
|
<option value="xdebug_peak_memory_usage" />
|
||||||
|
<option value="xdebug_print_function_stack" />
|
||||||
|
<option value="xdebug_start_code_coverage" />
|
||||||
|
<option value="xdebug_start_error_collection" />
|
||||||
|
<option value="xdebug_start_function_monitor" />
|
||||||
|
<option value="xdebug_start_trace" />
|
||||||
|
<option value="xdebug_stop_code_coverage" />
|
||||||
|
<option value="xdebug_stop_error_collection" />
|
||||||
|
<option value="xdebug_stop_function_monitor" />
|
||||||
|
<option value="xdebug_stop_trace" />
|
||||||
|
<option value="xdebug_time_index" />
|
||||||
|
<option value="xdebug_var_dump" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="migratedIntoUserSpace" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FunctionNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_regex" value="[a-z][A-Za-z]*" />
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="99" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FunctionWithInconsistentReturnsJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HashCodeUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HibernateResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="HtmlMissingClosingTag" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HtmlPresentationalElement" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="HtmlRequiredTitleAttribute" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myValues">
|
||||||
|
<value>
|
||||||
|
<list size="7">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="nobr" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="noembed" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="comment" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="noscript" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="embed" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="script" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="foreignobject" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myCustomValuesEnabled" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="IOResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredTypesString" value="java.io.ByteArrayOutputStream,java.io.ByteArrayInputStream,java.io.StringBufferInputStream,java.io.CharArrayWriter,java.io.CharArrayReader,java.io.StringWriter,java.io.StringReader" />
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="IfStatementWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ImplicitDefaultCharsetUsage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IncompleteDestructuring" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InconsistentLanguageLevel" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InconsistentLineSeparators" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IncrementDecrementOperationEquivalentInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="InnerClassOnInterface" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreInnerInterfaces" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreInvisibleFields" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="InsertLiteralUnderscores" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InstanceofCatchParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InstanceofIncompatibleInterface" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IsEmptyFunctionUsageInspection" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="SUGGEST_TO_USE_COUNT_CHECK" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="IsNullFunctionUsageInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IteratorNextDoesNotThrowNoSuchElementException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JDBCExecuteWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JDBCPrepareStatementWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JDBCResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JNDIResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JSClassNamingConvention" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="99" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JSConstructorReturnsPrimitive" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JSEqualityComparisonWithCoercion.TS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="mySeverity" value="Always" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JSJoinVariableDeclarationAndAssignment" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JSMissingSwitchBranches" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JSNonASCIINames" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myAllowOnlyAscii" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JSNonStrictModeUsed" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JSOctalInteger" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="myReportNonStrictEs5" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JSUndeclaredVariable" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="myCheckGlobalDefinitions" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JSUnusedGlobalSymbols" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myReportUnusedDefinitions" value="true" />
|
||||||
|
<option name="myReportUnusedProperties" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JUnitDatapoint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JUnitRule" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JUnitTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JavadocHtmlLint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JoinDeclarationAndAssignmentJava" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LambdaCanBeMethodCall" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LambdaUnfriendlyMethodOverload" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LengthOneStringInIndexOf" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LengthOneStringsInConcatenation" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ListenerMayUseAdapter" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="checkForEmptyMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="LoadLibraryWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="REPORT_VARIABLES" value="true" />
|
||||||
|
<option name="REPORT_PARAMETERS" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="LocalVariableDeclarationSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LocalVariableNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_regex" value="[a-z][A-Za-z0-9]*" />
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="99" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MalformedSetUpTearDown" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MapReplaceableByEnumMap" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodCallSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_onlyPrivateOrFinal" value="false" />
|
||||||
|
<option name="m_ignoreEmptyMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MethodMayBeSynchronized" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodOverloadsParentMethod" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="reportIncompatibleParameters" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MethodOverridesInaccessibleMethodOfSuper" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodOverridesStaticMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MisorderedAssertEqualsArguments" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MisorderedAssertEqualsArgumentsTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MisorderedAssertEqualsParameters" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MisorderedModifiersInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreObjectMethods" value="false" />
|
||||||
|
<option name="ignoreAnonymousClassMethods" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MissortedModifiers" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_requireAnnotationsFirst" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NakedNotify" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreNegatedNullComparison" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NestedAssignment" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NestedClassSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NestedSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NestedSynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NestedTernaryOperatorInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="NewExpressionSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonBlockStatementBodyJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonExceptionNameEndsWithException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalClone" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalFieldInEnum" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalFieldOfException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalStaticVariableUsedInClassInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonPublicClone" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonReproducibleMathCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonSerializableFieldInSerializableClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignorableAnnotations">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="ignoreAnonymousInnerClasses" value="false" />
|
||||||
|
<option name="superClassString" value="java.awt.Component" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NonSerializableObjectBoundToHttpSession" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonSerializableObjectPassedToObjectStream" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonSerializableWithSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonSerializableWithSerializationMethods" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonShortCircuitBoolean" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonSynchronizedMethodOverridesSynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonThreadSafeLazyInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NotOptimalIfConditionsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="REPORT_DUPLICATE_CONDITIONS" value="false" />
|
||||||
|
<option name="SUGGEST_OPTIMIZING_CONDITIONS" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NotifyCalledOnCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NotifyWithoutCorrespondingWait" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NullThrown" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NumericToString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="OCInconsistentNaming" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObjectAllocationIgnoredJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObjectInstantiationInEqualsHashCode" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObjectNotify" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObjectToString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObsoleteCollection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreRequiredObsoleteCollectionTypes" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="OctalAndDecimalIntegersMixed" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="OffsetOperationsInspection" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="OneTimeUseVariablesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ALLOW_LONG_STATEMENTS" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="OverloadedVarargsMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="OverridableMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PackageInMultipleModules" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ParameterHidingMemberVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreInvisibleFields" value="true" />
|
||||||
|
<option name="m_ignoreStaticMethodParametersHidingInstanceFields" value="true" />
|
||||||
|
<option name="m_ignoreForConstructors" value="true" />
|
||||||
|
<option name="m_ignoreForPropertySetters" value="true" />
|
||||||
|
<option name="m_ignoreForAbstractMethods" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ParameterNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_regex" value="[a-z][A-Za-z]*" />
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="99" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ParameterizedParametersStaticCollection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpAssignmentInConditionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpAssignmentReplaceableWithOperatorAssignmentInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpAssignmentReplaceableWithPrefixExpressionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpCSValidationInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||||
|
<option name="EXTENSIONS" value="php,js,css,inc" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpClassNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpClosureCanBeConvertedToShortArrowFunctionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpCompoundNamespaceDepthInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpConstantNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpConstantReassignmentInspection" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpDivisionByZeroInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="SKIP_ON_EMPTY_PHPDOC" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpFunctionNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpInconsistentReturnPointsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ALLOW_RETURN_NULL_IN_VOID" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpLongTypeFormInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpLoopCanBeConvertedToArrayFillInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpLoopCanBeConvertedToArrayFilterInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpLoopCanBeConvertedToArrayMapInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpMethodNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpMethodOrClassCallIsNotCaseSensitiveInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpMissingParentCallMagicInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpMissingStrictTypesDeclarationInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpMissingVisibilityInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpNewClassMissingParameterListInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpNonCanonicalElementsOrderInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpOverridingMethodVisibilityInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpPropertyNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_regex" value="[a-z][_a-z\d]*" />
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpRedundantClosingTagInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PhpSeparateElseIfInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpShortOpenTagInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpSingleStatementWithBracesInspection" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PhpStatementHasEmptyBodyInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myCommentsCountAsContent" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpStatementWithoutBracesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpStaticAsDynamicMethodCallInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="SHOW_FOR_MAGIC" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpTraditionalSyntaxArrayLiteralInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpTraitsUseListInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpUndefinedCallbackInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpUndefinedClassConstantInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="DOWNGRADE_SEVERITY" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpUndefinedMethodInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="DOWNGRADE_SEVERITY" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpUnnecessaryDoubleQuotesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpUnused" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="SHOW_UNUSED_BY_ENTRIES" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpUnusedParameterInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="DONT_REPORT_ANONYMOUS" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpVarUsageInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PhpVariableNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PhpVariableVariableInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PluginXmlDynamicPlugin" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="highlightNonDynamicEPUsages" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PointlessBitwiseExpressionJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreExpressionsContainingConstants" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PropertyCanBeStaticInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PublicFieldAccessedInSynchronizedContext" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PublicStaticArrayField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PublicStaticCollectionField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyAugmentAssignmentInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyClassicStyleClassInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyMissingTypeHintsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_onlyWhenTypesAreKnown" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="RawTypeCanBeGeneric" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReadObjectAndWriteObjectPrivate" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReadObjectInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReadResolveAndWriteReplaceProtected" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantElseClauseInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyWarnOnNull" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSerializable" value="true" />
|
||||||
|
<option name="ignoreCloneable" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="RedundantInnerClassModifier" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantObjectTypeCheck" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantSuppression" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="IGNORE_ALL" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ReferencingObjectsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="RegExpOctalEscape" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceAssignmentWithOperatorAssignment" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreLazyOperators" value="true" />
|
||||||
|
<option name="ignoreObscureOperators" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ReplaceAssignmentWithOperatorAssignmentJS" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceGuardClauseWithFunctionCall" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceStringFormatWithLiteral" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceSubstringWithDropLast" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceSubstringWithIndexingOperation" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceSubstringWithSubstringAfter" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceSubstringWithSubstringBefore" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceSubstringWithTake" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ResultOfObjectAllocationIgnored" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ResultSetIndexZero" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReturnOfInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RsSimplifyBooleanExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RuntimeExec" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RuntimeExecWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SafeLock" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SecurityAdvisoriesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="optionConfiguration">
|
||||||
|
<list>
|
||||||
|
<option value="barryvdh/laravel-debugbar" />
|
||||||
|
<option value="behat/behat" />
|
||||||
|
<option value="brianium/paratest" />
|
||||||
|
<option value="codeception/codeception" />
|
||||||
|
<option value="codedungeon/phpunit-result-printer" />
|
||||||
|
<option value="composer/composer" />
|
||||||
|
<option value="doctrine/coding-standard" />
|
||||||
|
<option value="filp/whoops" />
|
||||||
|
<option value="friendsofphp/php-cs-fixer" />
|
||||||
|
<option value="humbug/humbug" />
|
||||||
|
<option value="infection/infection" />
|
||||||
|
<option value="jakub-onderka/php-parallel-lint" />
|
||||||
|
<option value="johnkary/phpunit-speedtrap" />
|
||||||
|
<option value="kalessil/production-dependencies-guard" />
|
||||||
|
<option value="mikey179/vfsStream" />
|
||||||
|
<option value="mockery/mockery" />
|
||||||
|
<option value="mybuilder/phpunit-accelerator" />
|
||||||
|
<option value="orchestra/testbench" />
|
||||||
|
<option value="pdepend/pdepend" />
|
||||||
|
<option value="phan/phan" />
|
||||||
|
<option value="phing/phing" />
|
||||||
|
<option value="phpcompatibility/php-compatibility" />
|
||||||
|
<option value="phpmd/phpmd" />
|
||||||
|
<option value="phpro/grumphp" />
|
||||||
|
<option value="phpspec/phpspec" />
|
||||||
|
<option value="phpspec/prophecy" />
|
||||||
|
<option value="phpstan/phpstan" />
|
||||||
|
<option value="phpunit/phpunit" />
|
||||||
|
<option value="povils/phpmnd" />
|
||||||
|
<option value="roave/security-advisories" />
|
||||||
|
<option value="satooshi/php-coveralls" />
|
||||||
|
<option value="sebastian/phpcpd" />
|
||||||
|
<option value="slevomat/coding-standard" />
|
||||||
|
<option value="spatie/phpunit-watcher" />
|
||||||
|
<option value="squizlabs/php_codesniffer" />
|
||||||
|
<option value="sstalle/php7cc" />
|
||||||
|
<option value="symfony/debug" />
|
||||||
|
<option value="symfony/maker-bundle" />
|
||||||
|
<option value="symfony/phpunit-bridge" />
|
||||||
|
<option value="symfony/var-dumper" />
|
||||||
|
<option value="vimeo/psalm" />
|
||||||
|
<option value="wimg/php-compatibility" />
|
||||||
|
<option value="wp-coding-standards/wpcs" />
|
||||||
|
<option value="yiisoft/yii2-coding-standards" />
|
||||||
|
<option value="yiisoft/yii2-debug" />
|
||||||
|
<option value="yiisoft/yii2-gii" />
|
||||||
|
<option value="zendframework/zend-coding-standard" />
|
||||||
|
<option value="zendframework/zend-debug" />
|
||||||
|
<option value="zendframework/zend-test" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SerialPersistentFieldsWithWrongSignature" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SerialVersionUIDNotStaticFinal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SerializableHasSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreAnonymousInnerClasses" value="false" />
|
||||||
|
<option name="superClassString" value="java.awt.Component" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SerializableHasSerializationMethods" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreAnonymousInnerClasses" value="false" />
|
||||||
|
<option name="superClassString" value="java.awt.Component" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SerializableInnerClassHasSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreAnonymousInnerClasses" value="false" />
|
||||||
|
<option name="superClassString" value="java.awt.Component" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SerializableInnerClassWithNonSerializableOuterClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreAnonymousInnerClasses" value="false" />
|
||||||
|
<option name="superClassString" value="java.awt.Component" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SerializableStoresNonSerializable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SerializableWithUnconstructableAncestor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SeveralTargetsMessage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SharedThreadLocalRandom" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ShortEchoTagCanBeUsedInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SignalWithoutCorrespondingAwait" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SimpleDateFormatWithoutLocale" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SimplifiableAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SimplifiableIfStatement" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SingleStatementInBlock" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SleepWhileHoldingLock" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SocketResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SqlGotoInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SqlRedundantOrderingDirectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="SqlWithoutWhereInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myDontWarnForLimit" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StackEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StaticMethodOnlyUsedInOneClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreTestClasses" value="true" />
|
||||||
|
<option name="ignoreOnConflicts" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StrTrUsageAsStrReplaceInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="StringBufferToStringInConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringConcatenationInFormatCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringConcatenationInMessageFormatCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringConcatenationMissingWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringEqualsEmptyString" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="SUPPRESS_FOR_VALUES_WHICH_COULD_BE_NULL" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StringLiteralBreaksHTMLJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringReplaceableByStringBuffer" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyWarnOnLoop" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StringToUpperWithoutLocale" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SubStrUsedAsStrPosInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="SubtractionInCompareTo" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SuspiciousArrayCast" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SuspiciousIndentAfterControlStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SuspiciousLiteralUnderscore" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SwitchStatementWithConfusingDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizationOnStaticField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizeOnLock" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizeOnThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_includeNativeMethods" value="true" />
|
||||||
|
<option name="ignoreSynchronizedSuperMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SynchronizedOnLiteralObject" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SystemGC" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SystemGetenv" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SystemProperties" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SystemRunFinalizersOnExit" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SystemSetSecurityManager" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TailRecursionJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TestCaseInProductCode" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TestCaseWithConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TestCaseWithNoTestMethods" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSupers" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="TestMethodInProductCode" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TestMethodWithoutAssertion" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TestOnlyProblems" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TextLabelInSwitchStatementJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThreadDumpStack" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThreadLocalNotStaticFinal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThreadPriority" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThreadStartInConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThreadStopSuspendResume" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThreadYield" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThrowCaughtLocally" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreRethrownExceptions" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ThrowRawExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ThrowablePrintStackTrace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TimeToString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TooBroadScope" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_allowConstructorAsInitializer" value="false" />
|
||||||
|
<option name="m_onlyLookAtBlocks" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="TransientFieldInNonSerializableClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TransientFieldNotInitialized" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TrivialStringConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnNecessaryDoubleQuotesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnaryPlus" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnclearBinaryExpression" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="UnconditionalWait" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnconstrainedVariableType" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UndeclaredTests" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnknownInspectionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreStaticFieldAccesses" value="false" />
|
||||||
|
<option name="m_ignoreStaticMethodCalls" value="false" />
|
||||||
|
<option name="m_ignoreStaticAccessFromStaticContext" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessarilyQualifiedStaticallyImportedElement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryBoxing" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyReportSuperfluouslyBoxed" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreAnnotations" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryLocalVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreImmediatelyReturnedVariables" value="true" />
|
||||||
|
<option name="m_ignoreAnnotatedVariables" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryLocalVariableJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreImmediatelyReturnedVariables" value="true" />
|
||||||
|
<option name="m_ignoreAnnotatedVariables" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryParentheses" enabled="true" level="INFORMATION" enabled_by_default="true">
|
||||||
|
<option name="ignoreClarifyingParentheses" value="true" />
|
||||||
|
<option name="ignoreParenthesesOnConditionals" value="false" />
|
||||||
|
<option name="ignoreParenthesesOnLambdaParameter" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryUnaryMinus" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryUnboxing" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyReportSuperfluouslyUnboxed" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnsetConstructsCanBeMergedInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="UnterminatedStatementJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSemicolonAtEndOfBlock" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnusedCatchParameterJS" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreCatchBlocksWithComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UseCouple" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfAWTPeerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfJDBCDriverClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfObsoleteAssert" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfObsoleteDateTimeApi" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfProcessBuilder" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfPropertiesAsHashtable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfSunClasses" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UsingInclusionReturnValueInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UtilityClassWithPublicConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UtilityClassWithoutPrivateConstructor" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignorableAnnotations">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="ignoreClassesWithOnlyMain" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="VariableNotUsedInsideIf" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="VariableUseSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="VoidExpressionJS" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="VolatileArrayField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="W3CssValidation" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myCssVersion" value="css3svg" />
|
||||||
|
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="WaitCalledOnCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WaitNotInLoop" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WaitNotifyNotInSynchronizedContext" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WaitOrAwaitWithoutTimeout" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WaitWhileHoldingTwoLocks" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WaitWithoutCorrespondingNotify" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WhenWithOnlyElse" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="WhileLoopSpinsOnField" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreNonEmtpyLoops" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
10
app/.idea/.idea.DiscordHistoryTracker/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
app/.idea/.idea.DiscordHistoryTracker/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
app/.idea/.idea.DiscordHistoryTracker/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
app/.idea/.idea.DiscordHistoryTracker/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="PROJECT_PROFILE" value="Project" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
36
app/.idea/.idea.DiscordHistoryTracker/.idea/jsonSchemas.xml
generated
Normal file
36
app/.idea/.idea.DiscordHistoryTracker/.idea/jsonSchemas.xml
generated
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||||
|
<state>
|
||||||
|
<map>
|
||||||
|
<entry key="track-channel">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="track-channel" />
|
||||||
|
<option name="relativePathToSchema" value="Resources/Schemas/track-channel.yml" />
|
||||||
|
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="track-messages">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="track-messages" />
|
||||||
|
<option name="relativePathToSchema" value="Resources/Schemas/track-messages.yml" />
|
||||||
|
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="track-users">
|
||||||
|
<value>
|
||||||
|
<SchemaInfo>
|
||||||
|
<option name="name" value="track-users" />
|
||||||
|
<option name="relativePathToSchema" value="Resources/Schemas/track-users.yml" />
|
||||||
|
<option name="schemaVersion" value="JSON Schema version 7" />
|
||||||
|
</SchemaInfo>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
|
</project>
|
20
app/.idea/.idea.DiscordHistoryTracker/.idea/runConfigurations/Desktop.xml
generated
Normal file
20
app/.idea/.idea.DiscordHistoryTracker/.idea/runConfigurations/Desktop.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Desktop" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Desktop/debug/DiscordHistoryTracker.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.artifacts/bin/Desktop/debug" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Desktop/Desktop.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net8.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
3
app/.idea/.idea.DiscordHistoryTracker/.idea/scopes/Resources.xml
generated
Normal file
3
app/.idea/.idea.DiscordHistoryTracker/.idea/scopes/Resources.xml
generated
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<component name="DependencyValidationManager">
|
||||||
|
<scope name="Resources" pattern="file:Tracker/scripts/*" />
|
||||||
|
</component>
|
6
app/.idea/.idea.DiscordHistoryTracker/.idea/sqldialects.xml
generated
Normal file
6
app/.idea/.idea.DiscordHistoryTracker/.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="PROJECT" dialect="SQLite" />
|
||||||
|
</component>
|
||||||
|
</project>
|
11
app/.idea/.idea.DiscordHistoryTracker/.idea/vcs.xml
generated
Normal file
11
app/.idea/.idea.DiscordHistoryTracker/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitSharedSettings">
|
||||||
|
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
183
app/Desktop/App.axaml
Normal file
183
app/Desktop/App.axaml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:common="clr-namespace:DHT.Desktop.Common"
|
||||||
|
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||||
|
x:Class="DHT.Desktop.App"
|
||||||
|
RequestedThemeVariant="Light">
|
||||||
|
|
||||||
|
<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="Padding" Value="12" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TreeViewItem:not(:empty) /template/ Panel#PART_ExpandCollapseChevronContainer">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Margin" Value="0 0 5 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TreeViewItem:empty /template/ Panel#PART_ExpandCollapseChevronContainer">
|
||||||
|
<Setter Property="Margin" Value="10 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TreeViewItem[Level=0]:empty /template/ Panel#PART_ExpandCollapseChevronContainer">
|
||||||
|
<Setter Property="Margin" Value="0" />
|
||||||
|
<Setter Property="Width" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TreeViewItem /template/ ToggleButton#PART_ExpandCollapseChevron">
|
||||||
|
<Setter Property="Width" Value="18" />
|
||||||
|
<Setter Property="Height" Value="32" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TreeViewItem /template/ ToggleButton#PART_ExpandCollapseChevron > Border">
|
||||||
|
<Setter Property="Padding" Value="2 10 3 10" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TreeView.noSelection">
|
||||||
|
<Style Selector="^ TreeViewItem /template/ Border#PART_LayoutRoot">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource TreeViewItemBackground}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ TreeViewItem /template/ Border#PART_LayoutRoot > ContentPresenter#PART_HeaderPresenter">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TreeViewItemForeground}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ TreeViewItem:disabled /template/ Border#PART_LayoutRoot">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource TreeViewItemBackgroundDisabled}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushDisabled}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ TreeViewItem:disabled /template/ Border#PART_LayoutRoot > ContentPresenter#PART_HeaderPresenter">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundDisabled}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Panel.buttons">
|
||||||
|
<Setter Property="Margin" Value="0 20 0 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons > WrapPanel">
|
||||||
|
<Setter Property="ItemSpacing" Value="8" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Panel.buttons Button">
|
||||||
|
<Setter Property="MinWidth" Value="80" />
|
||||||
|
</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>
|
20
app/Desktop/App.axaml.cs
Normal file
20
app/Desktop/App.axaml.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using DHT.Desktop.Main;
|
||||||
|
|
||||||
|
namespace DHT.Desktop;
|
||||||
|
|
||||||
|
sealed class App : Application {
|
||||||
|
public override void Initialize() {
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted() {
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
|
||||||
|
desktop.MainWindow = new MainWindow(Program.Arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
87
app/Desktop/Arguments.cs
Normal file
87
app/Desktop/Arguments.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
|
namespace DHT.Desktop;
|
||||||
|
|
||||||
|
sealed class Arguments {
|
||||||
|
private static readonly Log Log = Log.ForType<Arguments>();
|
||||||
|
|
||||||
|
private const int FirstArgument = 1;
|
||||||
|
|
||||||
|
public static Arguments Empty => new ([]);
|
||||||
|
|
||||||
|
public bool Console { get; }
|
||||||
|
public string? DatabaseFile { get; }
|
||||||
|
public ushort? ServerPort { get; }
|
||||||
|
public string? ServerToken { get; }
|
||||||
|
public byte? ConcurrentDownloads { get; }
|
||||||
|
|
||||||
|
public Arguments(IReadOnlyList<string> args) {
|
||||||
|
for (int i = FirstArgument; i < args.Count; i++) {
|
||||||
|
string key = args[i];
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "-debug":
|
||||||
|
Log.IsDebugEnabled = true;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "-console":
|
||||||
|
Console = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string value;
|
||||||
|
|
||||||
|
if (i == FirstArgument && !key.StartsWith('-')) {
|
||||||
|
value = key;
|
||||||
|
key = "-db";
|
||||||
|
}
|
||||||
|
else if (i >= args.Count - 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 ushort port)) {
|
||||||
|
Log.Warn("Invalid port number: " + value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ServerPort = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "-token":
|
||||||
|
ServerToken = value;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "-concurrentdownloads":
|
||||||
|
if (!ulong.TryParse(value, out ulong concurrentDownloads) || concurrentDownloads == 0) {
|
||||||
|
Log.Warn("Invalid concurrent downloads count: " + value);
|
||||||
|
}
|
||||||
|
else if (concurrentDownloads > 10) {
|
||||||
|
Log.Warn("Limiting concurrent downloads to 10.");
|
||||||
|
ConcurrentDownloads = 10;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ConcurrentDownloads = (byte) concurrentDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Log.Warn("Unknown command line argument: " + key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
app/Desktop/Common/AvaloniaReflection.cs
Normal file
25
app/Desktop/Common/AvaloniaReflection.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Common;
|
||||||
|
|
||||||
|
static class AvaloniaReflection {
|
||||||
|
private static FieldInfo InteractiveEventHandlersField { get; } = typeof(Interactive).GetField("_eventHandlers", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
|
||||||
|
public static void Check() {
|
||||||
|
if (InteractiveEventHandlersField == null) {
|
||||||
|
throw new InvalidOperationException("Missing field: " + nameof(InteractiveEventHandlersField));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InteractiveEventHandlersField.FieldType.ToString() != "System.Collections.Generic.Dictionary`2[Avalonia.Interactivity.RoutedEvent,System.Collections.Generic.List`1[Avalonia.Interactivity.Interactive+EventSubscription]]") {
|
||||||
|
throw new InvalidOperationException("Invalid field type: " + nameof(InteractiveEventHandlersField) + " = " + InteractiveEventHandlersField.FieldType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IList? GetEventHandler(Interactive target, RoutedEvent routedEvent) {
|
||||||
|
IDictionary? eventHandlers = (IDictionary?) InteractiveEventHandlersField.GetValue(target);
|
||||||
|
return (IList?) eventHandlers?[routedEvent];
|
||||||
|
}
|
||||||
|
}
|
53
app/Desktop/Common/BytesValueConverter.cs
Normal file
53
app/Desktop/Common/BytesValueConverter.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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 Unit("B", decimalPlaces: 0),
|
||||||
|
new Unit("kB", decimalPlaces: 0),
|
||||||
|
new Unit("MB", decimalPlaces: 1),
|
||||||
|
new Unit("GB", decimalPlaces: 1),
|
||||||
|
new Unit("TB", decimalPlaces: 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
private const int Scale = 1000;
|
||||||
|
|
||||||
|
public 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();
|
||||||
|
}
|
||||||
|
}
|
69
app/Desktop/Common/DatabaseGui.cs
Normal file
69
app/Desktop/Common/DatabaseGui.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using DHT.Desktop.Dialogs.File;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Database.Exceptions;
|
||||||
|
using DHT.Server.Database.Sqlite;
|
||||||
|
using DHT.Server.Database.Sqlite.Schema;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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 = [
|
||||||
|
FileDialogs.CreateFilter("Discord History Tracker Database", ["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, ISchemaUpgradeCallbacks schemaUpgradeCallbacks) {
|
||||||
|
IDatabaseFile? file = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
file = await SqliteDatabaseFile.OpenOrCreate(path, schemaUpgradeCallbacks);
|
||||||
|
} 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("Could not open database file: " + path, 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/Common/NumberValueConverter.cs
Normal file
15
app/Desktop/Common/NumberValueConverter.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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();
|
||||||
|
}
|
||||||
|
}
|
9
app/Desktop/Common/SystemUtils.cs
Normal file
9
app/Desktop/Common/SystemUtils.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Common;
|
||||||
|
|
||||||
|
static class SystemUtils {
|
||||||
|
public static void OpenUrl(string url) {
|
||||||
|
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
}
|
19
app/Desktop/Common/TextFormat.cs
Normal file
19
app/Desktop/Common/TextFormat.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace DHT.Desktop.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");
|
||||||
|
}
|
||||||
|
}
|
47
app/Desktop/Desktop.csproj
Normal file
47
app/Desktop/Desktop.csproj
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>DHT.Desktop</RootNamespace>
|
||||||
|
<AssemblyName>DiscordHistoryTracker</AssemblyName>
|
||||||
|
<PackageId>DiscordHistoryTracker</PackageId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<ApplicationIcon>./Resources/icon.ico</ApplicationIcon>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||||
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.0" Condition=" '$(Configuration)' == 'Debug' " />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0" />
|
||||||
|
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.1.1">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Server\Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\Version.cs" Link="Version.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AvaloniaResource Include="Resources/icon.ico" />
|
||||||
|
<EmbeddedResource Include="Resources/tracker-loader.js">
|
||||||
|
<LogicalName>tracker-loader.js</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
51
app/Desktop/Dialogs/CheckBox/CheckBoxDialog.axaml
Normal file
51
app/Desktop/Dialogs/CheckBox/CheckBoxDialog.axaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<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:namespace="clr-namespace:DHT.Desktop.Dialogs.CheckBox"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.Dialogs.CheckBox.CheckBoxDialog"
|
||||||
|
x:DataType="namespace:CheckBoxDialogModel"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
MinWidth="425" MinHeight="200"
|
||||||
|
Width="500" Height="395" CanResize="True"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<namespace:CheckBoxDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="TreeViewItem">
|
||||||
|
<Setter Property="IsExpanded" Value="True" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<DockPanel Margin="20 17 20 20">
|
||||||
|
<Panel Classes="buttons" DockPanel.Dock="Bottom">
|
||||||
|
<WrapPanel>
|
||||||
|
<Button Command="{Binding SelectAll}" IsEnabled="{Binding !AreAllSelected}">Select All</Button>
|
||||||
|
<Button Command="{Binding SelectNone}" IsEnabled="{Binding !AreNoneSelected}">Select None</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
<WrapPanel HorizontalAlignment="Right">
|
||||||
|
<Button Click="ClickOk">OK</Button>
|
||||||
|
<Button Click="ClickCancel">Cancel</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Panel>
|
||||||
|
<ScrollViewer DockPanel.Dock="Top">
|
||||||
|
<TreeView Name="TreeView" Classes="noSelection" ItemsSource="{Binding RootItems}" ContainerPrepared="TreeViewOnContainerPrepared">
|
||||||
|
<TreeView.ItemTemplate>
|
||||||
|
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||||
|
<CheckBox IsChecked="{Binding IsChecked}">
|
||||||
|
<Label>
|
||||||
|
<TextBlock Text="{Binding Title}" TextWrapping="Wrap" />
|
||||||
|
</Label>
|
||||||
|
</CheckBox>
|
||||||
|
</TreeDataTemplate>
|
||||||
|
</TreeView.ItemTemplate>
|
||||||
|
</TreeView>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
</Window>
|
54
app/Desktop/Dialogs/CheckBox/CheckBoxDialog.axaml.cs
Normal file
54
app/Desktop/Dialogs/CheckBox/CheckBoxDialog.axaml.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class CheckBoxDialog : Window {
|
||||||
|
public CheckBoxDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TreeViewOnContainerPrepared(object? sender, ContainerPreparedEventArgs e) {
|
||||||
|
foreach (object? item in TreeView.Items) {
|
||||||
|
if (item != null && TreeView.ContainerFromItem(item) is TreeViewItem treeViewItem) {
|
||||||
|
treeViewItem.TemplateApplied += TreeViewItemOnTemplateApplied;
|
||||||
|
treeViewItem.GotFocus += TreeViewItemOnGotFocus;
|
||||||
|
treeViewItem.KeyDown += TreeViewItemOnKeyDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TreeViewItemOnTemplateApplied(object? sender, TemplateAppliedEventArgs e) {
|
||||||
|
if (sender is TreeViewItem { HeaderPresenter: Interactive headerPresenter } ) {
|
||||||
|
// Removes support for double-clicking to expand.
|
||||||
|
AvaloniaReflection.GetEventHandler(headerPresenter, DoubleTappedEvent)?.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TreeViewItemOnGotFocus(object? sender, GotFocusEventArgs e) {
|
||||||
|
if (e.NavigationMethod == NavigationMethod.Tab && sender is TreeViewItem treeViewItem && TreeView.SelectedItem == null) {
|
||||||
|
TreeView.SelectedItem = TreeView.ItemFromContainer(treeViewItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TreeViewItemOnKeyDown(object? sender, KeyEventArgs e) {
|
||||||
|
if (e.Key == Key.Space && TreeView.SelectedItem is ICheckBoxItem item) {
|
||||||
|
item.IsChecked = item.IsChecked == false;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickOk(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.OkCancel.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClickCancel(object? sender, RoutedEventArgs e) {
|
||||||
|
Close(DialogResult.OkCancel.Cancel);
|
||||||
|
}
|
||||||
|
}
|
73
app/Desktop/Dialogs/CheckBox/CheckBoxDialogModel.cs
Normal file
73
app/Desktop/Dialogs/CheckBox/CheckBoxDialogModel.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
partial class CheckBoxDialogModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
|
||||||
|
private ImmutableArray<ICheckBoxItem> rootItems = [];
|
||||||
|
|
||||||
|
public ImmutableArray<ICheckBoxItem> RootItems {
|
||||||
|
get => rootItems;
|
||||||
|
|
||||||
|
protected set {
|
||||||
|
foreach (ICheckBoxItem item in ICheckBoxItem.GetAllRecursively(rootItems)) {
|
||||||
|
item.PropertyChanged -= OnItemPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootItems = value;
|
||||||
|
|
||||||
|
foreach (ICheckBoxItem item in ICheckBoxItem.GetAllRecursively(rootItems)) {
|
||||||
|
item.PropertyChanged += OnItemPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IEnumerable<ICheckBoxItem> AllItems => ICheckBoxItem.GetAllRecursively(RootItems);
|
||||||
|
|
||||||
|
[DependsOn(nameof(RootItems))]
|
||||||
|
public bool AreAllSelected => RootItems.All(static item => item.IsChecked == true);
|
||||||
|
|
||||||
|
[DependsOn(nameof(RootItems))]
|
||||||
|
public bool AreNoneSelected => RootItems.All(static item => item.IsChecked == false);
|
||||||
|
|
||||||
|
private bool pauseUpdatingBulkButtons = false;
|
||||||
|
|
||||||
|
public void SelectAll() => SetAllChecked(true);
|
||||||
|
public void SelectNone() => SetAllChecked(false);
|
||||||
|
|
||||||
|
private void SetAllChecked(bool isChecked) {
|
||||||
|
pauseUpdatingBulkButtons = true;
|
||||||
|
|
||||||
|
foreach (ICheckBoxItem item in RootItems) {
|
||||||
|
item.IsChecked = isChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseUpdatingBulkButtons = false;
|
||||||
|
UpdateBulkButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(ICheckBoxItem.IsChecked) && !pauseUpdatingBulkButtons) {
|
||||||
|
UpdateBulkButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBulkButtons() {
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(RootItems)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class CheckBoxDialogModel<T> : CheckBoxDialogModel {
|
||||||
|
public IEnumerable<T> SelectedValues => AllItems.OfType<ICheckBoxItem.Leaf<T>>()
|
||||||
|
.Where(static item => item.IsChecked == true)
|
||||||
|
.Select(static item => item.Value);
|
||||||
|
|
||||||
|
public CheckBoxDialogModel(ImmutableArray<ICheckBoxItem> items) {
|
||||||
|
this.RootItems = items;
|
||||||
|
}
|
||||||
|
}
|
110
app/Desktop/Dialogs/CheckBox/CheckBoxItem.cs
Normal file
110
app/Desktop/Dialogs/CheckBox/CheckBoxItem.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
partial interface ICheckBoxItem : INotifyPropertyChanged {
|
||||||
|
public string Title { get; }
|
||||||
|
public bool? IsChecked { get; set; }
|
||||||
|
|
||||||
|
public ImmutableArray<ICheckBoxItem> Children { get; }
|
||||||
|
|
||||||
|
void NotifyIsCheckedChanged();
|
||||||
|
|
||||||
|
public static IEnumerable<ICheckBoxItem> GetAllRecursively(IEnumerable<ICheckBoxItem> items) {
|
||||||
|
Stack<ICheckBoxItem> stack = new Stack<ICheckBoxItem>(items);
|
||||||
|
|
||||||
|
while (stack.TryPop(out var item)) {
|
||||||
|
yield return item;
|
||||||
|
|
||||||
|
foreach (ICheckBoxItem child in item.Children) {
|
||||||
|
stack.Push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class NonLeaf : ICheckBoxItem {
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
public bool? IsChecked {
|
||||||
|
get {
|
||||||
|
if (Children.Count(static child => child.IsChecked == true) == Children.Length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (Children.Count(static child => child.IsChecked == false) == Children.Length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
foreach (ICheckBoxItem child in Children) {
|
||||||
|
if (child is Leaf leaf) {
|
||||||
|
leaf.SetCheckedFromParent(value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
child.IsChecked = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyIsCheckedChanged();
|
||||||
|
parent?.NotifyIsCheckedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<ICheckBoxItem> Children { get; }
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private readonly ICheckBoxItem? parent;
|
||||||
|
|
||||||
|
public NonLeaf(string title, ICheckBoxItem? parent, Func<ICheckBoxItem, ImmutableArray<ICheckBoxItem>> children) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.Title = title;
|
||||||
|
this.Children = children(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyIsCheckedChanged() {
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsChecked)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial class Leaf(string title, ICheckBoxItem? parent, bool isChecked) : ICheckBoxItem {
|
||||||
|
public string Title { get; } = title;
|
||||||
|
|
||||||
|
public ImmutableArray<ICheckBoxItem> Children => ImmutableArray<ICheckBoxItem>.Empty;
|
||||||
|
|
||||||
|
public readonly ICheckBoxItem? parent = parent;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool? isChecked = isChecked;
|
||||||
|
|
||||||
|
private bool notifyParent = true;
|
||||||
|
|
||||||
|
public void SetCheckedFromParent(bool? isChecked) {
|
||||||
|
notifyParent = false;
|
||||||
|
IsChecked = isChecked;
|
||||||
|
notifyParent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIsCheckedChanged() {
|
||||||
|
if (notifyParent) {
|
||||||
|
parent?.NotifyIsCheckedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICheckBoxItem.NotifyIsCheckedChanged() {
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsChecked)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Leaf<T>(string title, ICheckBoxItem? parent, T value, bool isChecked) : Leaf(title, parent, isChecked) {
|
||||||
|
public T Value => value;
|
||||||
|
}
|
||||||
|
}
|
46
app/Desktop/Dialogs/CheckBox/CheckBoxItemList.cs
Normal file
46
app/Desktop/Dialogs/CheckBox/CheckBoxItemList.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.CheckBox;
|
||||||
|
|
||||||
|
sealed class CheckBoxItemList<TKey, TValue> where TKey : notnull {
|
||||||
|
private readonly List<INode> rootNodes = [];
|
||||||
|
private readonly Dictionary<TKey, List<INode>> parentNodes = [];
|
||||||
|
|
||||||
|
public void AddParent(TKey key, string title) {
|
||||||
|
if (!parentNodes.ContainsKey(key)) {
|
||||||
|
List<INode> children = [];
|
||||||
|
rootNodes.Add(new INode.NonLeaf(title, children));
|
||||||
|
parentNodes[key] = children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(TValue value, string title, bool isChecked = false) {
|
||||||
|
rootNodes.Add(new INode.Leaf(title, value, isChecked));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(TKey key, TValue value, string title, bool isChecked = false) {
|
||||||
|
parentNodes.GetValueOrDefault(key, rootNodes).Add(new INode.Leaf(title, value, isChecked));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<ICheckBoxItem> ToCheckBoxItems() {
|
||||||
|
return [..rootNodes.Select(static node => node.ToCheckBoxItem(null))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface INode {
|
||||||
|
ICheckBoxItem ToCheckBoxItem(ICheckBoxItem? parent);
|
||||||
|
|
||||||
|
sealed record NonLeaf(string Title, List<INode> Children) : INode {
|
||||||
|
public ICheckBoxItem ToCheckBoxItem(ICheckBoxItem? parent) {
|
||||||
|
return new ICheckBoxItem.NonLeaf(Title, parent, self => [..Children.Select(child => child.ToCheckBoxItem(self))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed record Leaf(string Title, TValue Value, bool IsChecked) : INode {
|
||||||
|
public ICheckBoxItem ToCheckBoxItem(ICheckBoxItem? parent) {
|
||||||
|
return new ICheckBoxItem.Leaf<TValue>(Title, parent, Value, IsChecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
app/Desktop/Dialogs/File/FileDialogs.cs
Normal file
40
app/Desktop/Dialogs/File/FileDialogs.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.File;
|
||||||
|
|
||||||
|
static class FileDialogs {
|
||||||
|
public static async Task<string[]> OpenFolders(this IStorageProvider storageProvider, FolderPickerOpenOptions options) {
|
||||||
|
return (await storageProvider.OpenFolderPickerAsync(options)).ToLocalPaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 IStorageItem itme) {
|
||||||
|
return itme.TryGetLocalPath() ?? throw new NotSupportedException("Local filesystem is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] ToLocalPaths(this IReadOnlyList<IStorageItem> items) {
|
||||||
|
return items.Select(ToLocalPath).ToArray();
|
||||||
|
}
|
||||||
|
}
|
74
app/Desktop/Dialogs/Message/Dialog.cs
Normal file
74
app/Desktop/Dialogs/Message/Dialog.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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/Dialogs/Message/DialogResult.cs
Normal file
59
app/Desktop/Dialogs/Message/DialogResult.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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."),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
37
app/Desktop/Dialogs/Message/MessageDialog.axaml
Normal file
37
app/Desktop/Dialogs/Message/MessageDialog.axaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<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:namespace="clr-namespace:DHT.Desktop.Dialogs.Message"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.Dialogs.Message.MessageDialog"
|
||||||
|
x:DataType="namespace:MessageDialogModel"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<namespace:MessageDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="15" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<StackPanel Margin="20">
|
||||||
|
<TextBlock Text="{Binding Message}" />
|
||||||
|
<Panel Classes="buttons">
|
||||||
|
<WrapPanel HorizontalAlignment="Right">
|
||||||
|
<Button Click="ClickOk" IsVisible="{Binding IsOkVisible}">OK</Button>
|
||||||
|
<Button Click="ClickYes" IsVisible="{Binding IsYesVisible}">Yes</Button>
|
||||||
|
<Button Click="ClickNo" IsVisible="{Binding IsNoVisible}">No</Button>
|
||||||
|
<Button Click="ClickCancel" IsVisible="{Binding IsCancelVisible}">Cancel</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Window>
|
28
app/Desktop/Dialogs/Message/MessageDialog.axaml.cs
Normal file
28
app/Desktop/Dialogs/Message/MessageDialog.axaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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/Dialogs/Message/MessageDialogModel.cs
Normal file
11
app/Desktop/Dialogs/Message/MessageDialogModel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace DHT.Desktop.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;
|
||||||
|
}
|
24
app/Desktop/Dialogs/Progress/DelayedProgressDialog.cs
Normal file
24
app/Desktop/Dialogs/Progress/DelayedProgressDialog.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.Progress;
|
||||||
|
|
||||||
|
static class DelayedProgressDialog {
|
||||||
|
public static async ValueTask Await(Func<Task> taskProvider, TimeSpan delay, Window window, string progressDialogTitle, string progressDialogDescription) {
|
||||||
|
Dispatcher.UIThread.VerifyAccess();
|
||||||
|
|
||||||
|
Task task = Task.Run(taskProvider);
|
||||||
|
if (task.IsCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Freeze the UI thread for a short while in case the task finishes quickly.
|
||||||
|
_ = Task.WhenAny(Task.Delay(delay), task).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
if (!task.IsCompleted) {
|
||||||
|
await ProgressDialog.ShowIndeterminate(window, progressDialogTitle, progressDialogDescription, _ => task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
app/Desktop/Dialogs/Progress/IProgressCallback.cs
Normal file
9
app/Desktop/Dialogs/Progress/IProgressCallback.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.Progress;
|
||||||
|
|
||||||
|
interface IProgressCallback {
|
||||||
|
Task Update(string message, long finishedItems, long totalItems);
|
||||||
|
Task UpdateIndeterminate(string message);
|
||||||
|
Task Hide();
|
||||||
|
}
|
49
app/Desktop/Dialogs/Progress/ProgressDialog.axaml
Normal file
49
app/Desktop/Dialogs/Progress/ProgressDialog.axaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<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:namespace="clr-namespace:DHT.Desktop.Dialogs.Progress"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.Dialogs.Progress.ProgressDialog"
|
||||||
|
x:DataType="namespace:ProgressDialogModel"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Opened="OnOpened"
|
||||||
|
Closing="OnClosing"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<namespace: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>
|
||||||
|
|
||||||
|
<ItemsRepeater ItemsSource="{Binding Items}" Margin="0 10">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="20 10" IsHitTestVisible="{Binding IsVisible}" Opacity="{Binding Opacity}">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="{Binding Items}" Classes="items" />
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="{Binding Message}" />
|
||||||
|
</DockPanel>
|
||||||
|
<ProgressBar IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding Progress}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
|
||||||
|
</Window>
|
80
app/Desktop/Dialogs/Progress/ProgressDialog.axaml.cs
Normal file
80
app/Desktop/Dialogs/Progress/ProgressDialog.axaml.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.Progress;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class ProgressDialog : Window {
|
||||||
|
private static readonly TimeSpan MinimumShowDuration = TimeSpan.FromMilliseconds(500);
|
||||||
|
|
||||||
|
internal static async Task Show(Window owner, string title, Func<ProgressDialog, IProgressCallback, Task> action) {
|
||||||
|
var dialog = new ProgressDialog();
|
||||||
|
dialog.DataContext = new ProgressDialogModel(title, async callbacks => await action(dialog, callbacks[0]));
|
||||||
|
await dialog.ShowProgressDialog(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<T> Show<T>(Window owner, string title, Func<ProgressDialog, IProgressCallback, Task<T>> action) {
|
||||||
|
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||||
|
var dialog = new ProgressDialog();
|
||||||
|
|
||||||
|
dialog.DataContext = new ProgressDialogModel(title, async callbacks => {
|
||||||
|
taskCompletionSource.SetResult(await action(dialog, callbacks[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
await dialog.ShowProgressDialog(owner);
|
||||||
|
return await taskCompletionSource.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task ShowIndeterminate(Window owner, string title, string message, Func<ProgressDialog, Task> action) {
|
||||||
|
return Show(owner, title, async (dialog, callback) => {
|
||||||
|
await callback.UpdateIndeterminate(message);
|
||||||
|
await action(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task<T> ShowIndeterminate<T>(Window owner, string title, string message, Func<ProgressDialog, Task<T>> action) {
|
||||||
|
return Show<T>(owner, title, async (dialog, callback) => {
|
||||||
|
await callback.UpdateIndeterminate(message);
|
||||||
|
return await action(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isFinished = false;
|
||||||
|
private DateTime startTime = DateTime.Now;
|
||||||
|
private Task progressTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
public ProgressDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnOpened(object? sender, EventArgs e) {
|
||||||
|
startTime = DateTime.Now;
|
||||||
|
|
||||||
|
if (DataContext is ProgressDialogModel model) {
|
||||||
|
progressTask = Task.Run(model.StartTask);
|
||||||
|
progressTask.ContinueWith(OnFinished, TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClosing(object? sender, WindowClosingEventArgs e) {
|
||||||
|
e.Cancel = !isFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFinished(Task task) {
|
||||||
|
isFinished = true;
|
||||||
|
|
||||||
|
TimeSpan elapsedTime = DateTime.Now - startTime;
|
||||||
|
if (elapsedTime < MinimumShowDuration) {
|
||||||
|
await Task.Delay(MinimumShowDuration - elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ShowProgressDialog(Window owner) {
|
||||||
|
await ShowDialog(owner);
|
||||||
|
await progressTask;
|
||||||
|
}
|
||||||
|
}
|
63
app/Desktop/Dialogs/Progress/ProgressDialogModel.cs
Normal file
63
app/Desktop/Dialogs/Progress/ProgressDialogModel.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.Progress;
|
||||||
|
|
||||||
|
sealed class ProgressDialogModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
|
||||||
|
public IReadOnlyList<ProgressItem> Items { get; } = [];
|
||||||
|
|
||||||
|
private readonly TaskRunner? task;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public ProgressDialogModel() {}
|
||||||
|
|
||||||
|
public ProgressDialogModel(string title, TaskRunner task, int progressItems = 1) {
|
||||||
|
this.Title = title;
|
||||||
|
this.task = task;
|
||||||
|
this.Items = Enumerable.Range(start: 0, progressItems).Select(static _ => new ProgressItem()).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task StartTask() {
|
||||||
|
if (task != null) {
|
||||||
|
await task(Items.Select(static item => new Callback(item)).ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate Task TaskRunner(IReadOnlyList<IProgressCallback> callbacks);
|
||||||
|
|
||||||
|
private sealed class Callback : IProgressCallback {
|
||||||
|
private readonly ProgressItem item;
|
||||||
|
|
||||||
|
public Callback(ProgressItem item) {
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update(string message, long finishedItems, long totalItems) {
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
item.Message = message;
|
||||||
|
item.Items = totalItems == 0 ? string.Empty : finishedItems.Format() + " / " + totalItems.Format();
|
||||||
|
item.Progress = totalItems == 0 ? 0 : 100 * finishedItems / totalItems;
|
||||||
|
item.IsIndeterminate = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateIndeterminate(string message) {
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
item.Message = message;
|
||||||
|
item.Items = string.Empty;
|
||||||
|
item.Progress = 0;
|
||||||
|
item.IsIndeterminate = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Hide() {
|
||||||
|
return Update(string.Empty, finishedItems: 0, totalItems: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
app/Desktop/Dialogs/Progress/ProgressItem.cs
Normal file
23
app/Desktop/Dialogs/Progress/ProgressItem.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.Progress;
|
||||||
|
|
||||||
|
sealed partial class ProgressItem {
|
||||||
|
[Notify]
|
||||||
|
private string message = "";
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string items = "";
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private long progress = 0L;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool isIndeterminate;
|
||||||
|
|
||||||
|
[DependsOn(nameof(Message))]
|
||||||
|
public bool IsVisible => !string.IsNullOrEmpty(Message);
|
||||||
|
|
||||||
|
[DependsOn(nameof(IsVisible))]
|
||||||
|
public double Opacity => IsVisible ? 1.0 : 0.0;
|
||||||
|
}
|
44
app/Desktop/Dialogs/TextBox/TextBoxDialog.axaml
Normal file
44
app/Desktop/Dialogs/TextBox/TextBoxDialog.axaml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<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:namespace="clr-namespace:DHT.Desktop.Dialogs.TextBox"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="500"
|
||||||
|
x:Class="DHT.Desktop.Dialogs.TextBox.TextBoxDialog"
|
||||||
|
x:DataType="namespace:TextBoxDialogModel"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="500" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<namespace:TextBoxDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<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 HorizontalAlignment="Right">
|
||||||
|
<Button Click="ClickOk" IsEnabled="{Binding !HasErrors}">OK</Button>
|
||||||
|
<Button Click="ClickCancel">Cancel</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Window>
|
21
app/Desktop/Dialogs/TextBox/TextBoxDialog.axaml.cs
Normal file
21
app/Desktop/Dialogs/TextBox/TextBoxDialog.axaml.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.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/Dialogs/TextBox/TextBoxDialogModel.cs
Normal file
47
app/Desktop/Dialogs/TextBox/TextBoxDialogModel.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.TextBox;
|
||||||
|
|
||||||
|
partial class TextBoxDialogModel {
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
public string Description { get; init; } = "";
|
||||||
|
|
||||||
|
private IReadOnlyList<TextBoxItem> items = [];
|
||||||
|
|
||||||
|
public IReadOnlyList<TextBoxItem> Items {
|
||||||
|
get => items;
|
||||||
|
|
||||||
|
protected set {
|
||||||
|
foreach (TextBoxItem item in items) {
|
||||||
|
item.ErrorsChanged -= OnItemErrorsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = value;
|
||||||
|
|
||||||
|
foreach (TextBoxItem item in items) {
|
||||||
|
item.ErrorsChanged += OnItemErrorsChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DependsOn(nameof(Items))]
|
||||||
|
public bool HasErrors => Items.Any(static item => !item.IsValid);
|
||||||
|
|
||||||
|
private void OnItemErrorsChanged(object? sender, DataErrorsChangedEventArgs e) {
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Items)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class TextBoxDialogModel<T> : TextBoxDialogModel {
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
}
|
39
app/Desktop/Dialogs/TextBox/TextBoxItem.cs
Normal file
39
app/Desktop/Dialogs/TextBox/TextBoxItem.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Dialogs.TextBox;
|
||||||
|
|
||||||
|
partial class TextBoxItem : 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);
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string value = string.Empty;
|
||||||
|
|
||||||
|
private void OnValueChanged() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
118
app/Desktop/Discord/DiscordAppSettings.cs
Normal file
118
app/Desktop/Discord/DiscordAppSettings.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
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 {
|
||||||
|
JsonObject settingsJson = await ReadSettingsJson();
|
||||||
|
return AreDevToolsEnabled(settingsJson);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not read settings file.", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AreDevToolsEnabled(JsonObject json) {
|
||||||
|
return json.TryGetPropertyValue(JsonKeyDevTools, out JsonNode? node) && node?.GetValueKind() == JsonValueKind.True;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<SettingsJsonResult> ConfigureDevTools(bool enable) {
|
||||||
|
JsonObject json;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json = await ReadSettingsJson();
|
||||||
|
} catch (FileNotFoundException) {
|
||||||
|
return SettingsJsonResult.FileNotFound;
|
||||||
|
} catch (JsonException) {
|
||||||
|
return SettingsJsonResult.InvalidJson;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not read settings file.", 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("Could not write settings file.", e);
|
||||||
|
|
||||||
|
if (File.Exists(JsonBackupFilePath)) {
|
||||||
|
try {
|
||||||
|
File.Move(JsonBackupFilePath, JsonFilePath, overwrite: true);
|
||||||
|
Log.Info("Restored settings file from backup.");
|
||||||
|
} catch (Exception e2) {
|
||||||
|
Log.Error("Could not restore settings file from backup.", e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsJsonResult.WriteError;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File.Delete(JsonBackupFilePath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not delete backup file.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsJsonResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<JsonObject> ReadSettingsJson() {
|
||||||
|
await using var stream = new FileStream(JsonFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
return await JsonSerializer.DeserializeAsync(stream, DiscordAppSettingsJsonContext.Default.JsonObject) ?? throw new JsonException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WriteSettingsJson(JsonObject json) {
|
||||||
|
await using var stream = new FileStream(JsonFilePath, FileMode.Truncate, FileAccess.Write, FileShare.None);
|
||||||
|
await JsonSerializer.SerializeAsync(stream, json, DiscordAppSettingsJsonContext.Default.JsonObject);
|
||||||
|
}
|
||||||
|
}
|
8
app/Desktop/Discord/DiscordAppSettingsJsonContext.cs
Normal file
8
app/Desktop/Discord/DiscordAppSettingsJsonContext.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Discord;
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default, WriteIndented = true)]
|
||||||
|
[JsonSerializable(typeof(JsonObject))]
|
||||||
|
sealed partial class DiscordAppSettingsJsonContext : JsonSerializerContext;
|
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,
|
||||||
|
}
|
84
app/Desktop/Main/AboutWindow.axaml
Normal file
84
app/Desktop/Main/AboutWindow.axaml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<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="510" d:DesignHeight="425"
|
||||||
|
x:Class="DHT.Desktop.Main.AboutWindow"
|
||||||
|
x:DataType="main:AboutWindowModel"
|
||||||
|
Title="About Discord History Tracker"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="510" Height="425" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<main:AboutWindowModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Grid > Button">
|
||||||
|
<Setter Property="Margin" Value="0 3" />
|
||||||
|
<Setter Property="Padding" Value="8 4" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Margin="20" Spacing="12">
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="3">
|
||||||
|
<TextBlock TextWrapping="Wrap">Discord History Tracker was created by chylex.</TextBlock>
|
||||||
|
<TextBlock>It is available under the MIT license.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Command="{Binding OpenOfficialWebsite}">Official Website</Button>
|
||||||
|
<Button Command="{Binding OpenIssueTracker}">Issue Tracker</Button>
|
||||||
|
<Button Command="{Binding OpenSourceCode}">Source Code</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,5,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="*,115,90" Margin="0 10 0 0">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" FontWeight="Bold">Third-Party Software</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="2" Grid.Column="0">.NET</TextBlock>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1">MIT</TextBlock>
|
||||||
|
<Button Grid.Row="2" Grid.Column="2" Command="{Binding OpenThirdPartyNetCore}">GitHub</Button>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0">Avalonia</TextBlock>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1">MIT</TextBlock>
|
||||||
|
<Button Grid.Row="3" Grid.Column="2" Command="{Binding OpenThirdPartyAvalonia}">GitHub</Button>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0">Rx.NET</TextBlock>
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="1">MIT</TextBlock>
|
||||||
|
<Button Grid.Row="4" Grid.Column="2" Command="{Binding OpenThirdPartyRxNet}">GitHub</Button>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0">SQLite</TextBlock>
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="1">Public Domain</TextBlock>
|
||||||
|
<Button Grid.Row="5" Grid.Column="2" Command="{Binding OpenThirdPartySqlite}">Website</Button>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="0">Microsoft.Data.Sqlite</TextBlock>
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="1">Apache-2.0</TextBlock>
|
||||||
|
<Button Grid.Row="6" Grid.Column="2" Command="{Binding OpenThirdPartyMicrosoftDataSqlite}">NuGet</Button>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="7" Grid.Column="0">PropertyChanged.SourceGenerator</TextBlock>
|
||||||
|
<TextBlock Grid.Row="7" Grid.Column="1">MIT</TextBlock>
|
||||||
|
<Button Grid.Row="7" Grid.Column="2" Command="{Binding OpenThirdPartyPropertyChangedSourceGenerator}">GitHub</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,5,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="*,115,90" Margin="0 5 0 0">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" FontWeight="Bold">Additional Thanks</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="2" Grid.Column="0">BetterDiscord, for Webpack hooks</TextBlock>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1">Apache-2.0</TextBlock>
|
||||||
|
<Button Grid.Row="2" Grid.Column="2" Command="{Binding OpenThirdPartyBetterDiscord}">GitHub</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
11
app/Desktop/Main/AboutWindow.axaml.cs
Normal file
11
app/Desktop/Main/AboutWindow.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class AboutWindow : Window {
|
||||||
|
public AboutWindow() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
45
app/Desktop/Main/AboutWindowModel.cs
Normal file
45
app/Desktop/Main/AboutWindowModel.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using DHT.Desktop.Common;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
|
sealed class AboutWindowModel {
|
||||||
|
public void OpenOfficialWebsite() {
|
||||||
|
SystemUtils.OpenUrl(Program.Website);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenIssueTracker() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/chylex/Discord-History-Tracker/issues");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenSourceCode() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/chylex/Discord-History-Tracker");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartyNetCore() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/dotnet/core");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartyAvalonia() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/AvaloniaUI/Avalonia");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartyPropertyChangedSourceGenerator() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/canton7/PropertyChanged.SourceGenerator");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartySqlite() {
|
||||||
|
SystemUtils.OpenUrl("https://www.sqlite.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartyMicrosoftDataSqlite() {
|
||||||
|
SystemUtils.OpenUrl("https://www.nuget.org/packages/Microsoft.Data.Sqlite");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartyRxNet() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/dotnet/reactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenThirdPartyBetterDiscord() {
|
||||||
|
SystemUtils.OpenUrl("https://github.com/BetterDiscord/BetterDiscord");
|
||||||
|
}
|
||||||
|
}
|
48
app/Desktop/Main/Controls/DownloadItemFilterPanel.axaml
Normal file
48
app/Desktop/Main/Controls/DownloadItemFilterPanel.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.Main.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Class="DHT.Desktop.Main.Controls.DownloadItemFilterPanel"
|
||||||
|
x:DataType="controls:DownloadItemFilterPanelModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<controls:DownloadItemFilterPanelModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="WrapPanel > StackPanel">
|
||||||
|
<Setter Property="Spacing" Value="4" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Grid > Label">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ComboBox">
|
||||||
|
<Setter Property="Margin" Value="8 0 0 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="Margin" Value="0 0 0 8" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Spacing="20">
|
||||||
|
<TextBlock Text="{Binding FilterStatisticsText}" />
|
||||||
|
<WrapPanel ItemSpacing="40" LineSpacing="20">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox IsChecked="{Binding LimitSize}">Limit Size</CheckBox>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBox Text="{Binding MaximumSize}" IsEnabled="{Binding LimitSize}" HorizontalContentAlignment="Right" />
|
||||||
|
<ComboBox IsEnabled="{Binding LimitSize}" ItemsSource="{Binding Units}" SelectedItem="{Binding MaximumSizeUnit}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Name}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Controls/DownloadItemFilterPanel.axaml.cs
Normal file
11
app/Desktop/Main/Controls/DownloadItemFilterPanel.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class DownloadItemFilterPanel : UserControl {
|
||||||
|
public DownloadItemFilterPanel() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
170
app/Desktop/Main/Controls/DownloadItemFilterPanelModel.cs
Normal file
170
app/Desktop/Main/Controls/DownloadItemFilterPanelModel.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Data.Filters;
|
||||||
|
using DHT.Server.Data.Settings;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using DHT.Utils.Tasks;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
sealed partial class DownloadItemFilterPanelModel : IAsyncDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<DownloadItemFilterPanelModel>();
|
||||||
|
|
||||||
|
public sealed record Unit(string Name, uint Scale);
|
||||||
|
|
||||||
|
private static readonly Unit[] AllUnits = [
|
||||||
|
new Unit("B", Scale: 1),
|
||||||
|
new Unit("kB", Scale: 1024),
|
||||||
|
new Unit("MB", Scale: 1024 * 1024)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly HashSet<string> FilterProperties = [
|
||||||
|
nameof(LimitSize),
|
||||||
|
nameof(MaximumSize),
|
||||||
|
nameof(MaximumSizeUnit),
|
||||||
|
];
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private string filterStatisticsText = "";
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool limitSize = false;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private ulong maximumSize = 0UL;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private Unit maximumSizeUnit = AllUnits[0];
|
||||||
|
|
||||||
|
public IEnumerable<Unit> Units => AllUnits;
|
||||||
|
|
||||||
|
private readonly State state;
|
||||||
|
private readonly string verb;
|
||||||
|
|
||||||
|
private readonly DelayedThrottledTask<FilterSettings> saveFilterSettingsTask;
|
||||||
|
private bool isLoadingFilterSettings;
|
||||||
|
|
||||||
|
private readonly RestartableTask<long> downloadItemCountTask;
|
||||||
|
private long? matchingItemCount;
|
||||||
|
|
||||||
|
private readonly IDisposable downloadItemCountSubscription;
|
||||||
|
private long? totalItemCount;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public DownloadItemFilterPanelModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
|
public DownloadItemFilterPanelModel(State state, string verb = "Matches") {
|
||||||
|
this.state = state;
|
||||||
|
this.verb = verb;
|
||||||
|
|
||||||
|
this.saveFilterSettingsTask = new DelayedThrottledTask<FilterSettings>(Log, TimeSpan.FromSeconds(5), SaveFilterSettings);
|
||||||
|
|
||||||
|
this.downloadItemCountTask = new RestartableTask<long>(SetMatchingCount, TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
|
this.downloadItemCountSubscription = state.Db.Downloads.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(OnDownloadItemCountChanged);
|
||||||
|
|
||||||
|
UpdateFilterStatistics();
|
||||||
|
|
||||||
|
PropertyChanged += OnPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize() {
|
||||||
|
isLoadingFilterSettings = true;
|
||||||
|
|
||||||
|
LimitSize = await state.Db.Settings.Get(SettingsKey.DownloadsLimitSize, LimitSize);
|
||||||
|
MaximumSize = await state.Db.Settings.Get(SettingsKey.DownloadsMaximumSize, MaximumSize);
|
||||||
|
|
||||||
|
if (await state.Db.Settings.Get(SettingsKey.DownloadsMaximumSizeUnit, defaultValue: null) is {} unitName && AllUnits.FirstOrDefault(unit => unit.Name == unitName) is {} unitValue) {
|
||||||
|
MaximumSizeUnit = unitValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingFilterSettings = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
saveFilterSettingsTask.Dispose();
|
||||||
|
|
||||||
|
downloadItemCountTask.Cancel();
|
||||||
|
downloadItemCountSubscription.Dispose();
|
||||||
|
|
||||||
|
await SaveFilterSettings(new FilterSettings(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record FilterSettings(bool LimitSize, ulong MaximumSize, Unit MaximumSizeUnit) {
|
||||||
|
public FilterSettings(DownloadItemFilterPanelModel model) : this(model.LimitSize, model.MaximumSize, model.MaximumSizeUnit) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveFilterSettings(FilterSettings settings) {
|
||||||
|
try {
|
||||||
|
await state.Db.Settings.Set(async setter => {
|
||||||
|
await setter.Set(SettingsKey.DownloadsLimitSize, settings.LimitSize);
|
||||||
|
await setter.Set(SettingsKey.DownloadsMaximumSize, settings.MaximumSize);
|
||||||
|
await setter.Set(SettingsKey.DownloadsMaximumSizeUnit, settings.MaximumSizeUnit.Name);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not save download filter settings.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName != null && FilterProperties.Contains(e.PropertyName)) {
|
||||||
|
if (!isLoadingFilterSettings) {
|
||||||
|
saveFilterSettingsTask.Post(new FilterSettings(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateFilterStatistics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDownloadItemCountChanged(long newItemCount) {
|
||||||
|
totalItemCount = newItemCount;
|
||||||
|
UpdateFilterStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void UpdateFilterStatistics() {
|
||||||
|
DownloadItemFilter filter = CreateFilter();
|
||||||
|
if (filter.IsEmpty) {
|
||||||
|
downloadItemCountTask.Cancel();
|
||||||
|
matchingItemCount = totalItemCount;
|
||||||
|
UpdateFilterStatisticsText();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
matchingItemCount = null;
|
||||||
|
UpdateFilterStatisticsText();
|
||||||
|
downloadItemCountTask.Restart(cancellationToken => state.Db.Downloads.Count(filter, cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMatchingCount(long matchingAttachmentCount) {
|
||||||
|
this.matchingItemCount = matchingAttachmentCount;
|
||||||
|
UpdateFilterStatisticsText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFilterStatisticsText() {
|
||||||
|
string matchingItemCountStr = matchingItemCount?.Format() ?? "(...)";
|
||||||
|
string totalItemCountStr = totalItemCount?.Format() ?? "(...)";
|
||||||
|
FilterStatisticsText = verb + " " + matchingItemCountStr + " out of " + totalItemCountStr + " file" + (totalItemCount is null or 1 ? "." : "s.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadItemFilter CreateFilter() {
|
||||||
|
DownloadItemFilter filter = new ();
|
||||||
|
|
||||||
|
if (LimitSize) {
|
||||||
|
try {
|
||||||
|
filter.MaxBytes = maximumSize * maximumSizeUnit.Scale;
|
||||||
|
} catch (ArithmeticException) {
|
||||||
|
// set no size limit, because the overflown size is larger than any file could possibly be
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
}
|
60
app/Desktop/Main/Controls/MessageFilterPanel.axaml
Normal file
60
app/Desktop/Main/Controls/MessageFilterPanel.axaml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<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.Main.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Class="DHT.Desktop.Main.Controls.MessageFilterPanel"
|
||||||
|
x:DataType="controls:MessageFilterPanelModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<controls:MessageFilterPanelModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="WrapPanel > StackPanel">
|
||||||
|
<Setter Property="Spacing" Value="4" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Grid > Label">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Grid > CalendarDatePicker">
|
||||||
|
<Setter Property="CornerRadius" Value="0" />
|
||||||
|
<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>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="Margin" Value="0 0 0 8" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Spacing="20">
|
||||||
|
<TextBlock Text="{Binding FilterStatisticsText}" />
|
||||||
|
<WrapPanel ItemSpacing="40" LineSpacing="20">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox IsChecked="{Binding FilterByDate}">Filter by Date</CheckBox>
|
||||||
|
<Grid ColumnDefinitions="Auto, 4, 125" RowDefinitions="Auto, 4, Auto" Margin="4 0">
|
||||||
|
<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>
|
||||||
|
<CheckBox IsChecked="{Binding FilterByChannel}">Filter by Channel</CheckBox>
|
||||||
|
<Button Command="{Binding OpenChannelFilterDialog}" IsEnabled="{Binding FilterByChannel}">Select Channels...</Button>
|
||||||
|
<TextBlock Text="{Binding ChannelFilterLabel}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox IsChecked="{Binding FilterByUser}">Filter by User</CheckBox>
|
||||||
|
<Button Command="{Binding OpenUserFilterDialog}" IsEnabled="{Binding FilterByUser}">Select Users...</Button>
|
||||||
|
<TextBlock Text="{Binding UserFilterLabel}" />
|
||||||
|
</StackPanel>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
27
app/Desktop/Main/Controls/MessageFilterPanel.axaml.cs
Normal file
27
app/Desktop/Main/Controls/MessageFilterPanel.axaml.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class MessageFilterPanel : UserControl {
|
||||||
|
public MessageFilterPanel() {
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
CultureInfo culture = Program.Culture;
|
||||||
|
foreach (CalendarDatePicker picker in new CalendarDatePicker[] { StartDatePicker, EndDatePicker }) {
|
||||||
|
picker.FirstDayOfWeek = culture.DateTimeFormat.FirstDayOfWeek;
|
||||||
|
picker.SelectedDateFormat = CalendarDatePickerFormat.Custom;
|
||||||
|
picker.CustomDateFormatString = culture.DateTimeFormat.ShortDatePattern;
|
||||||
|
picker.Watermark = culture.DateTimeFormat.ShortDatePattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CalendarDatePicker_OnSelectedDateChanged(object? sender, SelectionChangedEventArgs e) {
|
||||||
|
if (DataContext is MessageFilterPanelModel model) {
|
||||||
|
model.StartDate = StartDatePicker.SelectedDate;
|
||||||
|
model.EndDate = EndDatePicker.SelectedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
324
app/Desktop/Main/Controls/MessageFilterPanelModel.cs
Normal file
324
app/Desktop/Main/Controls/MessageFilterPanelModel.cs
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Desktop.Dialogs.CheckBox;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Data;
|
||||||
|
using DHT.Server.Data.Filters;
|
||||||
|
using DHT.Utils.Tasks;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
sealed partial class MessageFilterPanelModel : IDisposable {
|
||||||
|
private static readonly HashSet<string> FilterProperties = [
|
||||||
|
nameof(FilterByDate),
|
||||||
|
nameof(StartDate),
|
||||||
|
nameof(EndDate),
|
||||||
|
nameof(FilterByChannel),
|
||||||
|
nameof(IncludedChannels),
|
||||||
|
nameof(FilterByUser),
|
||||||
|
nameof(IncludedUsers),
|
||||||
|
];
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? FilterPropertyChanged;
|
||||||
|
|
||||||
|
public bool HasAnyFilters => FilterByDate || FilterByChannel || FilterByUser;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string filterStatisticsText = "";
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool filterByDate = false;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private DateTime? startDate = null;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private DateTime? endDate = null;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool filterByChannel = false;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private HashSet<ulong>? includedChannels = null;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool filterByUser = false;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private HashSet<ulong>? includedUsers = null;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string channelFilterLabel = "";
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string userFilterLabel = "";
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly State state;
|
||||||
|
private readonly string verb;
|
||||||
|
|
||||||
|
private readonly RestartableTask<long> exportedMessageCountTask;
|
||||||
|
private long? exportedMessageCount;
|
||||||
|
|
||||||
|
private readonly IDisposable messageCountSubscription;
|
||||||
|
private long? totalMessageCount;
|
||||||
|
|
||||||
|
private readonly IDisposable channelCountSubscription;
|
||||||
|
private long? totalChannelCount;
|
||||||
|
|
||||||
|
private readonly IDisposable userCountSubscription;
|
||||||
|
private long? totalUserCount;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public MessageFilterPanelModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public MessageFilterPanelModel(Window window, State state, string verb = "Matches") {
|
||||||
|
this.window = window;
|
||||||
|
this.state = state;
|
||||||
|
this.verb = verb;
|
||||||
|
|
||||||
|
this.exportedMessageCountTask = new RestartableTask<long>(SetExportedMessageCount, TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
|
|
||||||
|
this.messageCountSubscription = state.Db.Messages.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(OnMessageCountChanged);
|
||||||
|
this.channelCountSubscription = state.Db.Channels.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(OnChannelCountChanged);
|
||||||
|
this.userCountSubscription = state.Db.Users.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(OnUserCountChanged);
|
||||||
|
|
||||||
|
UpdateFilterStatistics();
|
||||||
|
UpdateChannelFilterLabel();
|
||||||
|
UpdateUserFilterLabel();
|
||||||
|
|
||||||
|
PropertyChanged += OnPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
exportedMessageCountTask.Cancel();
|
||||||
|
|
||||||
|
messageCountSubscription.Dispose();
|
||||||
|
channelCountSubscription.Dispose();
|
||||||
|
userCountSubscription.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName != null && FilterProperties.Contains(e.PropertyName)) {
|
||||||
|
UpdateFilterStatistics();
|
||||||
|
FilterPropertyChanged?.Invoke(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.PropertyName is nameof(FilterByChannel) or nameof(IncludedChannels)) {
|
||||||
|
UpdateChannelFilterLabel();
|
||||||
|
}
|
||||||
|
else if (e.PropertyName is nameof(FilterByUser) or nameof(IncludedUsers)) {
|
||||||
|
UpdateUserFilterLabel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMessageCountChanged(long newMessageCount) {
|
||||||
|
totalMessageCount = newMessageCount;
|
||||||
|
UpdateFilterStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChannelCountChanged(long newChannelCount) {
|
||||||
|
totalChannelCount = newChannelCount;
|
||||||
|
UpdateChannelFilterLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserCountChanged(long newUserCount) {
|
||||||
|
totalUserCount = newUserCount;
|
||||||
|
UpdateUserFilterLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateChannelFilterLabel() {
|
||||||
|
if (totalChannelCount.HasValue) {
|
||||||
|
long total = totalChannelCount.Value;
|
||||||
|
long included = FilterByChannel && IncludedChannels != null ? IncludedChannels.Count : total;
|
||||||
|
ChannelFilterLabel = "Selected " + included.Format() + " / " + total.Pluralize("channel") + ".";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ChannelFilterLabel = "Loading...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUserFilterLabel() {
|
||||||
|
if (totalUserCount.HasValue) {
|
||||||
|
long total = totalUserCount.Value;
|
||||||
|
long included = FilterByUser && IncludedUsers != null ? IncludedUsers.Count : total;
|
||||||
|
UserFilterLabel = "Selected " + included.Format() + " / " + total.Pluralize("user") + ".";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
UserFilterLabel = "Loading...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFilterStatistics() {
|
||||||
|
MessageFilter filter = CreateFilter();
|
||||||
|
if (filter.IsEmpty) {
|
||||||
|
exportedMessageCountTask.Cancel();
|
||||||
|
exportedMessageCount = totalMessageCount;
|
||||||
|
UpdateFilterStatisticsText();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
exportedMessageCount = null;
|
||||||
|
UpdateFilterStatisticsText();
|
||||||
|
exportedMessageCountTask.Restart(cancellationToken => state.Db.Messages.Count(filter, cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetExportedMessageCount(long exportedMessageCount) {
|
||||||
|
this.exportedMessageCount = exportedMessageCount;
|
||||||
|
UpdateFilterStatisticsText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFilterStatisticsText() {
|
||||||
|
string exportedMessageCountStr = exportedMessageCount?.Format() ?? "(...)";
|
||||||
|
string totalMessageCountStr = totalMessageCount?.Format() ?? "(...)";
|
||||||
|
FilterStatisticsText = verb + " " + exportedMessageCountStr + " out of " + totalMessageCountStr + " message" + (totalMessageCount is null or 1 ? "." : "s.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
|
||||||
|
private readonly record struct ChannelFilterKey(byte Type, ulong? ServerId, string Title) : IComparable<ChannelFilterKey> {
|
||||||
|
public static ChannelFilterKey DirectMessages { get; } = new (Type: 1, ServerId: null, Title: "Direct Messages");
|
||||||
|
public static ChannelFilterKey GroupMessages { get; } = new (Type: 2, ServerId: null, Title: "Group Messages");
|
||||||
|
public static ChannelFilterKey Unknown { get; } = new (Type: 4, ServerId: null, Title: "Unknown");
|
||||||
|
|
||||||
|
public static ChannelFilterKey For(DHT.Server.Data.Server server) {
|
||||||
|
return server.Type switch {
|
||||||
|
ServerType.Server => new ChannelFilterKey(Type: 3, server.Id, "Server - " + server.Name),
|
||||||
|
ServerType.Group => GroupMessages,
|
||||||
|
ServerType.DirectMessage => DirectMessages,
|
||||||
|
_ => Unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(ChannelFilterKey other) {
|
||||||
|
return Type == other.Type && ServerId == other.ServerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return HashCode.Combine(Type, ServerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(ChannelFilterKey other) {
|
||||||
|
int result = Type.CompareTo(other.Type);
|
||||||
|
if (result != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Title.CompareTo(other.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OpenChannelFilterDialog() {
|
||||||
|
async Task<ImmutableArray<ICheckBoxItem>> PrepareChannelItems(ProgressDialog dialog) {
|
||||||
|
CheckBoxItemList<ChannelFilterKey, ulong> items = new CheckBoxItemList<ChannelFilterKey, ulong>();
|
||||||
|
Dictionary<ulong, DHT.Server.Data.Server> servers = await state.Db.Servers.Get().ToDictionaryAsync(static server => server.Id);
|
||||||
|
|
||||||
|
foreach (ChannelFilterKey channelFilterKey in servers.Values.Select(ChannelFilterKey.For).Order()) {
|
||||||
|
items.AddParent(channelFilterKey, channelFilterKey.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
await foreach (Channel channel in state.Db.Channels.Get().OrderBy(static channel => channel.Position ?? int.MinValue).ThenBy(static channel => channel.Name)) {
|
||||||
|
ChannelFilterKey key = servers.TryGetValue(channel.Server, out var server)
|
||||||
|
? ChannelFilterKey.For(server)
|
||||||
|
: ChannelFilterKey.Unknown;
|
||||||
|
|
||||||
|
items.Add(key, channel.Id, channel.Name, isChecked: IncludedChannels == null || IncludedChannels.Contains(channel.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.ToCheckBoxItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
const string Title = "Included Channels";
|
||||||
|
|
||||||
|
ImmutableArray<ICheckBoxItem> items;
|
||||||
|
try {
|
||||||
|
items = await ProgressDialog.ShowIndeterminate(window, Title, "Loading channels...", PrepareChannelItems);
|
||||||
|
} catch (Exception e) {
|
||||||
|
await Dialog.ShowOk(window, Title, "Error loading channels: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<ulong>? result = await OpenIdFilterDialog(Title, items);
|
||||||
|
if (result != null) {
|
||||||
|
IncludedChannels = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OpenUserFilterDialog() {
|
||||||
|
async Task<ImmutableArray<ICheckBoxItem>> PrepareUserItems(ProgressDialog dialog) {
|
||||||
|
CheckBoxItemList<ulong, ulong> items = new CheckBoxItemList<ulong, ulong>();
|
||||||
|
|
||||||
|
static string GetDisplayName(User user) {
|
||||||
|
return user.DisplayName == null ? user.Name : $"{user.DisplayName} ({user.Name})";
|
||||||
|
}
|
||||||
|
|
||||||
|
await foreach ((ulong id, string name) in state.Db.Users.Get().Select(static user => (user.Id, GetDisplayName(user))).OrderBy(static pair => pair.Item2)) {
|
||||||
|
items.Add(
|
||||||
|
value: id,
|
||||||
|
title: name,
|
||||||
|
isChecked: IncludedUsers == null || IncludedUsers.Contains(id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.ToCheckBoxItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
const string Title = "Included Users";
|
||||||
|
|
||||||
|
ImmutableArray<ICheckBoxItem> items;
|
||||||
|
try {
|
||||||
|
items = await ProgressDialog.ShowIndeterminate(window, Title, "Loading users...", PrepareUserItems);
|
||||||
|
} catch (Exception e) {
|
||||||
|
await Dialog.ShowOk(window, Title, "Error loading users: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<ulong>? result = await OpenIdFilterDialog(Title, items);
|
||||||
|
if (result != null) {
|
||||||
|
IncludedUsers = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HashSet<ulong>?> OpenIdFilterDialog(string title, ImmutableArray<ICheckBoxItem> items) {
|
||||||
|
var model = new CheckBoxDialogModel<ulong>(items) {
|
||||||
|
Title = title,
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = new CheckBoxDialog { DataContext = model };
|
||||||
|
var result = await dialog.ShowDialog<DialogResult.OkCancel>(window);
|
||||||
|
|
||||||
|
return result == DialogResult.OkCancel.Ok ? model.SelectedValues.ToHashSet() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageFilter CreateFilter() {
|
||||||
|
MessageFilter filter = new ();
|
||||||
|
|
||||||
|
if (FilterByDate) {
|
||||||
|
filter.StartDate = StartDate;
|
||||||
|
filter.EndDate = EndDate?.AddDays(1).AddMilliseconds(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FilterByChannel && IncludedChannels != null) {
|
||||||
|
filter.ChannelIds = new HashSet<ulong>(IncludedChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FilterByUser && IncludedUsers != null) {
|
||||||
|
filter.UserIds = new HashSet<ulong>(IncludedUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
}
|
52
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml
Normal file
52
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<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.Main.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Class="DHT.Desktop.Main.Controls.ServerConfigurationPanel"
|
||||||
|
x:DataType="controls:ServerConfigurationPanelModel">
|
||||||
|
|
||||||
|
<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="Label">
|
||||||
|
<Setter Property="Margin" Value="0 5" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="WrapPanel > StackPanel">
|
||||||
|
<Setter Property="Orientation" Value="Vertical" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<Button Command="{Binding OnClickToggleServerButton}" Content="{Binding ToggleServerButtonText}" IsEnabled="{Binding IsToggleServerButtonEnabled}" />
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0 5">
|
||||||
|
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 or connection code again.
|
||||||
|
</TextBlock>
|
||||||
|
<WrapPanel ItemSpacing="10" LineSpacing="10">
|
||||||
|
<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/Main/Controls/ServerConfigurationPanel.axaml.cs
Normal file
11
app/Desktop/Main/Controls/ServerConfigurationPanel.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class ServerConfigurationPanel : UserControl {
|
||||||
|
public ServerConfigurationPanel() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
118
app/Desktop/Main/Controls/ServerConfigurationPanelModel.cs
Normal file
118
app/Desktop/Main/Controls/ServerConfigurationPanelModel.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
sealed partial class ServerConfigurationPanelModel : IDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<ServerConfigurationPanelModel>();
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string inputPort;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private string inputToken;
|
||||||
|
|
||||||
|
[DependsOn(nameof(InputPort), nameof(InputToken))]
|
||||||
|
public bool HasMadeChanges => ServerConfiguration.Port.ToString() != InputPort || ServerConfiguration.Token != InputToken;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool isToggleServerButtonEnabled = true;
|
||||||
|
|
||||||
|
public string ToggleServerButtonText => server.IsRunning ? "Stop Server" : "Start Server";
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly ServerManager server;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public ServerConfigurationPanelModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public ServerConfigurationPanelModel(Window window, State state) {
|
||||||
|
this.window = window;
|
||||||
|
this.server = state.Server;
|
||||||
|
this.inputPort = ServerConfiguration.Port.ToString();
|
||||||
|
this.inputToken = ServerConfiguration.Token;
|
||||||
|
|
||||||
|
server.StatusChanged += OnServerStatusChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
server.StatusChanged -= OnServerStatusChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServerStatusChanged(object? sender, ServerManager.Status e) {
|
||||||
|
Dispatcher.UIThread.InvokeAsync(UpdateServerStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateServerStatus() {
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ToggleServerButtonText)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartServer() {
|
||||||
|
IsToggleServerButtonEnabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.Start(ServerConfiguration.Port, ServerConfiguration.Token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not start internal server.", e);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateServerStatus();
|
||||||
|
IsToggleServerButtonEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopServer() {
|
||||||
|
IsToggleServerButtonEnabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.Stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not stop internal server.", e);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateServerStatus();
|
||||||
|
IsToggleServerButtonEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickToggleServerButton() {
|
||||||
|
if (server.IsRunning) {
|
||||||
|
await StopServer();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await StartServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClickRandomizeToken() {
|
||||||
|
InputToken = ServerUtils.GenerateRandomToken(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickApplyChanges() {
|
||||||
|
if (!ushort.TryParse(InputPort, out ushort port)) {
|
||||||
|
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerConfiguration.Port = port;
|
||||||
|
ServerConfiguration.Token = inputToken;
|
||||||
|
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(HasMadeChanges)));
|
||||||
|
|
||||||
|
await StartServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClickCancelChanges() {
|
||||||
|
InputPort = ServerConfiguration.Port.ToString();
|
||||||
|
InputToken = ServerConfiguration.Token;
|
||||||
|
}
|
||||||
|
}
|
62
app/Desktop/Main/Controls/StatusBar.axaml
Normal file
62
app/Desktop/Main/Controls/StatusBar.axaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<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.Main.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Class="DHT.Desktop.Main.Controls.StatusBar"
|
||||||
|
x:DataType="controls:StatusBarModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<controls:StatusBarModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Background>
|
||||||
|
<SolidColorBrush>#3C4F79</SolidColorBrush>
|
||||||
|
</UserControl.Background>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="StackPanel > TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#E7E7E7" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="StackPanel > TextBlock.label">
|
||||||
|
<Setter Property="FontSize" Value="15" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiLight" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="StackPanel > TextBlock.value">
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiLight" />
|
||||||
|
<Setter Property="TextAlignment" Value="Right" />
|
||||||
|
<Setter Property="Margin" Value="0 1" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="StackPanel > Rectangle">
|
||||||
|
<Setter Property="Margin" Value="14 1" />
|
||||||
|
<Setter Property="Stroke" Value="#697899" />
|
||||||
|
<Setter Property="StrokeThickness" Value="2" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="6 3">
|
||||||
|
<StackPanel Orientation="Vertical" Width="65">
|
||||||
|
<TextBlock Classes="label">Status</TextBlock>
|
||||||
|
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding ServerStatusText}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Rectangle />
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Classes="label">Servers</TextBlock>
|
||||||
|
<TextBlock Classes="value" Text="{Binding ServerCount, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Rectangle />
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Classes="label">Channels</TextBlock>
|
||||||
|
<TextBlock Classes="value" Text="{Binding ChannelCount, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Rectangle />
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Classes="label">Messages</TextBlock>
|
||||||
|
<TextBlock Classes="value" Text="{Binding MessageCount, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Controls/StatusBar.axaml.cs
Normal file
11
app/Desktop/Main/Controls/StatusBar.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class StatusBar : UserControl {
|
||||||
|
public StatusBar() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
63
app/Desktop/Main/Controls/StatusBarModel.cs
Normal file
63
app/Desktop/Main/Controls/StatusBarModel.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
|
sealed partial class StatusBarModel : IDisposable {
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private long? serverCount;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private long? channelCount;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private long? messageCount;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private ServerManager.Status serverStatus;
|
||||||
|
|
||||||
|
[DependsOn(nameof(ServerStatus))]
|
||||||
|
public string ServerStatusText => ServerStatus switch {
|
||||||
|
ServerManager.Status.Starting => "STARTING",
|
||||||
|
ServerManager.Status.Started => "READY",
|
||||||
|
ServerManager.Status.Stopping => "STOPPING",
|
||||||
|
ServerManager.Status.Stopped => "STOPPED",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly State state;
|
||||||
|
private readonly IDisposable serverCountSubscription;
|
||||||
|
private readonly IDisposable channelCountSubscription;
|
||||||
|
private readonly IDisposable messageCountSubscription;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public StatusBarModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
|
public StatusBarModel(State state) {
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
serverCountSubscription = state.Db.Servers.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(newServerCount => ServerCount = newServerCount);
|
||||||
|
channelCountSubscription = state.Db.Channels.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(newChannelCount => ChannelCount = newChannelCount);
|
||||||
|
messageCountSubscription = state.Db.Messages.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(newMessageCount => MessageCount = newMessageCount);
|
||||||
|
|
||||||
|
state.Server.StatusChanged += OnStateServerStatusChanged;
|
||||||
|
serverStatus = state.Server.IsRunning ? ServerManager.Status.Started : ServerManager.Status.Stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
serverCountSubscription.Dispose();
|
||||||
|
channelCountSubscription.Dispose();
|
||||||
|
messageCountSubscription.Dispose();
|
||||||
|
|
||||||
|
state.Server.StatusChanged -= OnStateServerStatusChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStateServerStatusChanged(object? sender, ServerManager.Status e) {
|
||||||
|
Dispatcher.UIThread.InvokeAsync(() => ServerStatus = e);
|
||||||
|
}
|
||||||
|
}
|
57
app/Desktop/Main/Dialogs/NewDatabaseSettingsDialog.axaml
Normal file
57
app/Desktop/Main/Dialogs/NewDatabaseSettingsDialog.axaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<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:namespace="clr-namespace:DHT.Desktop.Main.Dialogs"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="450"
|
||||||
|
x:Class="DHT.Desktop.Main.Dialogs.NewDatabaseSettingsDialog"
|
||||||
|
x:DataType="namespace:NewDatabaseSettingsDialogModel"
|
||||||
|
Title="New Database Settings"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Closing="OnClosing"
|
||||||
|
Width="450" SizeToContent="Height" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Window.DataContext>
|
||||||
|
<namespace:NewDatabaseSettingsDialogModel />
|
||||||
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Label > StackPanel">
|
||||||
|
<Setter Property="Margin" Value="0 4 0 0" />
|
||||||
|
<Setter Property="Orientation" Value="Vertical" />
|
||||||
|
<Setter Property="Spacing" Value="5" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.dim">
|
||||||
|
<Setter Property="Foreground" Value="DimGray" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Margin="20">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="15">
|
||||||
|
<CheckBox IsChecked="{Binding SeparateFileForDownloads}">
|
||||||
|
<Label>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock>Use a Separate Database File for Downloads</TextBlock>
|
||||||
|
<TextBlock Classes="dim">Downloaded files will be stored separately from messages. This cannot be changed later.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Label>
|
||||||
|
</CheckBox>
|
||||||
|
<CheckBox IsChecked="{Binding DownloadsAutoStart}">
|
||||||
|
<Label>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock>Download Files Automatically</TextBlock>
|
||||||
|
<TextBlock Classes="dim">Enables automatic downloads of files hosted on Discord. This can be changed later in the Downloads tab.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Label>
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
<Panel Classes="buttons">
|
||||||
|
<WrapPanel HorizontalAlignment="Right">
|
||||||
|
<Button Click="ClickOk">OK</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Window>
|
22
app/Desktop/Main/Dialogs/NewDatabaseSettingsDialog.axaml.cs
Normal file
22
app/Desktop/Main/Dialogs/NewDatabaseSettingsDialog.axaml.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Dialogs;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class NewDatabaseSettingsDialog : Window {
|
||||||
|
public NewDatabaseSettingsDialog() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClosing(object? sender, WindowClosingEventArgs e) {
|
||||||
|
if (!e.IsProgrammatic) {
|
||||||
|
e.Cancel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClickOk(object? sender, RoutedEventArgs e) {
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
11
app/Desktop/Main/Dialogs/NewDatabaseSettingsDialogModel.cs
Normal file
11
app/Desktop/Main/Dialogs/NewDatabaseSettingsDialogModel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Dialogs;
|
||||||
|
|
||||||
|
sealed partial class NewDatabaseSettingsDialogModel {
|
||||||
|
[Notify]
|
||||||
|
private bool separateFileForDownloads = true;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool downloadsAutoStart = true;
|
||||||
|
}
|
23
app/Desktop/Main/MainWindow.axaml
Normal file
23
app/Desktop/Main/MainWindow.axaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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"
|
||||||
|
x:DataType="main:MainWindowModel"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Icon="avares://DiscordHistoryTracker/Resources/icon.ico"
|
||||||
|
Width="820" Height="520"
|
||||||
|
MinWidth="520" MinHeight="300"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Closing="OnClosing">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<main:MainWindowModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<ContentPresenter Content="{Binding CurrentScreen}" />
|
||||||
|
</Panel>
|
||||||
|
</Window>
|
45
app/Desktop/Main/MainWindow.axaml.cs
Normal file
45
app/Desktop/Main/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class MainWindow : Window {
|
||||||
|
private static readonly Log Log = Log.ForType<MainWindow>();
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public MainWindow() {
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = new MainWindowModel(this, Arguments.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal MainWindow(Arguments args) {
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = new MainWindowModel(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OnClosing(object? sender, WindowClosingEventArgs e) {
|
||||||
|
e.Cancel = true;
|
||||||
|
Closing -= OnClosing;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Dispose();
|
||||||
|
} finally {
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Dispose() {
|
||||||
|
if (DataContext is MainWindowModel model) {
|
||||||
|
try {
|
||||||
|
await model.DisposeAsync();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error("Caught exception while disposing window: " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
app/Desktop/Main/MainWindowModel.cs
Normal file
144
app/Desktop/Main/MainWindowModel.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Desktop.Main.Screens;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main;
|
||||||
|
|
||||||
|
sealed partial class MainWindowModel : IAsyncDisposable {
|
||||||
|
private const string DefaultTitle = "Discord History Tracker";
|
||||||
|
|
||||||
|
private static readonly Log Log = Log.ForType<MainWindowModel>();
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private string title = DefaultTitle;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private UserControl currentScreen;
|
||||||
|
|
||||||
|
private readonly WelcomeScreen welcomeScreen;
|
||||||
|
private readonly WelcomeScreenModel welcomeScreenModel;
|
||||||
|
|
||||||
|
private MainContentScreenModel? mainContentScreenModel;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly int? concurrentDownloads;
|
||||||
|
|
||||||
|
private State? state;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public MainWindowModel() : this(null!, Arguments.Empty) {}
|
||||||
|
|
||||||
|
public MainWindowModel(Window window, Arguments args) {
|
||||||
|
this.window = window;
|
||||||
|
|
||||||
|
welcomeScreenModel = new WelcomeScreenModel(window);
|
||||||
|
welcomeScreenModel.DatabaseSelected += OnDatabaseSelected;
|
||||||
|
|
||||||
|
welcomeScreen = new WelcomeScreen { DataContext = welcomeScreenModel };
|
||||||
|
currentScreen = welcomeScreen;
|
||||||
|
|
||||||
|
string? 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) {
|
||||||
|
ServerConfiguration.Port = args.ServerPort.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.ServerToken != null) {
|
||||||
|
ServerConfiguration.Token = args.ServerToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
concurrentDownloads = args.ConcurrentDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnDatabaseSelected(object? sender, IDatabaseFile db) {
|
||||||
|
welcomeScreenModel.DatabaseSelected -= OnDatabaseSelected;
|
||||||
|
|
||||||
|
await DisposeState();
|
||||||
|
|
||||||
|
state = new State(db, concurrentDownloads);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await state.Server.Start(ServerConfiguration.Port, ServerConfiguration.Token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error("Could not start internal server.", ex);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mainContentScreenModel = new MainContentScreenModel(window, state);
|
||||||
|
mainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||||
|
await mainContentScreenModel.Initialize();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error("Could not initialize content screen.", ex);
|
||||||
|
await Dialog.ShowOk(window, "Initialization Error", ex.Message);
|
||||||
|
await DisposeContent();
|
||||||
|
await DisposeState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Title = Path.GetFileName(state.Db.Path) + " - " + DefaultTitle;
|
||||||
|
CurrentScreen = new MainContentScreen { DataContext = mainContentScreenModel };
|
||||||
|
|
||||||
|
window.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void MainContentScreenModelOnDatabaseClosed(object? sender, EventArgs e) {
|
||||||
|
await DisposeContent();
|
||||||
|
|
||||||
|
Title = DefaultTitle;
|
||||||
|
CurrentScreen = welcomeScreen;
|
||||||
|
|
||||||
|
await DisposeState();
|
||||||
|
|
||||||
|
welcomeScreenModel.DatabaseSelected += OnDatabaseSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DisposeContent() {
|
||||||
|
if (mainContentScreenModel != null) {
|
||||||
|
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||||
|
await mainContentScreenModel.DisposeAsync();
|
||||||
|
mainContentScreenModel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DisposeState() {
|
||||||
|
if (state != null) {
|
||||||
|
await DelayedProgressDialog.Await(() => state.DisposeAsync().AsTask(), TimeSpan.FromMilliseconds(200), window, "Close Database", "Please wait for the database to close...");
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
await DisposeContent();
|
||||||
|
await DisposeState();
|
||||||
|
}
|
||||||
|
}
|
20
app/Desktop/Main/Pages/AdvancedPage.axaml
Normal file
20
app/Desktop/Main/Pages/AdvancedPage.axaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<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"
|
||||||
|
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.Main.Pages.AdvancedPage"
|
||||||
|
x:DataType="pages:AdvancedPageModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:AdvancedPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<Expander Header="Internal Server Configuration" IsExpanded="True">
|
||||||
|
<controls:ServerConfigurationPanel DataContext="{Binding ServerConfigurationModel}" />
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Pages/AdvancedPage.axaml.cs
Normal file
11
app/Desktop/Main/Pages/AdvancedPage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class AdvancedPage : UserControl {
|
||||||
|
public AdvancedPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
21
app/Desktop/Main/Pages/AdvancedPageModel.cs
Normal file
21
app/Desktop/Main/Pages/AdvancedPageModel.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.Main.Controls;
|
||||||
|
using DHT.Server;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed class AdvancedPageModel : IDisposable {
|
||||||
|
public ServerConfigurationPanelModel ServerConfigurationModel { get; }
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public AdvancedPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public AdvancedPageModel(Window window, State state) {
|
||||||
|
ServerConfigurationModel = new ServerConfigurationPanelModel(window, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
ServerConfigurationModel.Dispose();
|
||||||
|
}
|
||||||
|
}
|
32
app/Desktop/Main/Pages/DatabasePage.axaml
Normal file
32
app/Desktop/Main/Pages/DatabasePage.axaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<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.DatabasePage"
|
||||||
|
x:DataType="pages:DatabasePageModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:DatabasePageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<DockPanel>
|
||||||
|
<Button Command="{Binding CloseDatabase}" DockPanel.Dock="Right">Close Database</Button>
|
||||||
|
<TextBox Text="{Binding Db.Path}" Width="NaN" Margin="0 0 10 0" IsReadOnly="True" />
|
||||||
|
</DockPanel>
|
||||||
|
<WrapPanel ItemSpacing="10" LineSpacing="10">
|
||||||
|
<Button Command="{Binding OpenDatabaseFolder}">Open Database Folder</Button>
|
||||||
|
<Button Command="{Binding MergeWithDatabase}">Merge with Database(s)...</Button>
|
||||||
|
<Button Command="{Binding ImportLegacyArchive}">Import Legacy Archive(s)...</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
<Expander Header="Advanced Tools" Margin="0 25 0 0">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<TextBlock TextWrapping="Wrap">Recreate the database to free up space after deleting data.</TextBlock>
|
||||||
|
<Button Command="{Binding VacuumDatabase}">Vacuum Database</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Pages/DatabasePage.axaml.cs
Normal file
11
app/Desktop/Main/Pages/DatabasePage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class DatabasePage : UserControl {
|
||||||
|
public DatabasePage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
312
app/Desktop/Main/Pages/DatabasePageModel.cs
Normal file
312
app/Desktop/Main/Pages/DatabasePageModel.cs
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Desktop.Dialogs.File;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Desktop.Dialogs.TextBox;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Data;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Database.Import;
|
||||||
|
using DHT.Server.Database.Sqlite.Schema;
|
||||||
|
using DHT.Server.Database.Sqlite.Utils;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed class DatabasePageModel {
|
||||||
|
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!, State.Dummy) {}
|
||||||
|
|
||||||
|
public DatabasePageModel(Window window, State state) {
|
||||||
|
this.window = window;
|
||||||
|
this.Db = state.Db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task 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", [ folder ]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlatformID.MacOSX:
|
||||||
|
Process.Start("open", [ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MergeWithDatabase() {
|
||||||
|
string[] paths = await DatabaseGui.NewOpenDatabaseFilesDialog(window, Path.GetDirectoryName(Db.Path));
|
||||||
|
if (paths.Length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string Title = "Database Merge";
|
||||||
|
|
||||||
|
ImportResult? result;
|
||||||
|
try {
|
||||||
|
result = await ProgressDialog.Show(window, Title, async (dialog, callback) => await MergeWithDatabaseFromPaths(Db, paths, dialog, callback));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not merge databases.", e);
|
||||||
|
await Dialog.ShowOk(window, Title, "Could not merge databases: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Dialog.ShowOk(window, Title, GetImportDialogMessage(result, "database file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ImportResult?> MergeWithDatabaseFromPaths(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback) {
|
||||||
|
var schemaUpgradeCallbacks = new SchemaUpgradeCallbacks(dialog, paths.Length);
|
||||||
|
|
||||||
|
return await PerformImport(target, paths, dialog, callback, "Database Merge", async path => {
|
||||||
|
IDatabaseFile? db = await DatabaseGui.TryOpenOrCreateDatabaseFromPath(path, dialog, schemaUpgradeCallbacks);
|
||||||
|
|
||||||
|
if (db == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await target.AddFrom(db);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
await db.DisposeAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SchemaUpgradeCallbacks(ProgressDialog dialog, int total) : ISchemaUpgradeCallbacks {
|
||||||
|
private bool? decision;
|
||||||
|
|
||||||
|
public Task<InitialDatabaseSettings?> GetInitialDatabaseSettings() {
|
||||||
|
return Task.FromResult<InitialDatabaseSettings?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanUpgrade() {
|
||||||
|
return decision ??= (total > 1
|
||||||
|
? await DatabaseGui.ShowCanUpgradeMultipleDatabaseDialog(dialog)
|
||||||
|
: await DatabaseGui.ShowCanUpgradeDatabaseDialog(dialog)) == DialogResult.YesNo.Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Start(int versionSteps, Func<ISchemaUpgradeCallbacks.IProgressReporter, Task> doUpgrade) {
|
||||||
|
return doUpgrade(new NullReporter());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullReporter : ISchemaUpgradeCallbacks.IProgressReporter {
|
||||||
|
public Task NextVersion() {
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task MainWork(string message, int finishedItems, int totalItems) {
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SubWork(string message, int finishedItems, int totalItems) {
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ImportLegacyArchive() {
|
||||||
|
string[] paths = await window.StorageProvider.OpenFiles(new FilePickerOpenOptions {
|
||||||
|
Title = "Open Legacy DHT Archive",
|
||||||
|
SuggestedStartLocation = await FileDialogs.GetSuggestedStartLocation(window, Path.GetDirectoryName(Db.Path)),
|
||||||
|
AllowMultiple = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paths.Length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string Title = "Legacy Archive Import";
|
||||||
|
|
||||||
|
ImportResult? result;
|
||||||
|
try {
|
||||||
|
result = await ProgressDialog.Show(window, Title, async (dialog, callback) => await ImportLegacyArchiveFromPaths(Db, paths, dialog, callback));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not import legacy archives.", e);
|
||||||
|
await Dialog.ShowOk(window, Title, "Could not import legacy archives: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Dialog.ShowOk(window, Title, GetImportDialogMessage(result, "archive file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ImportResult?> ImportLegacyArchiveFromPaths(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback) {
|
||||||
|
var fakeSnowflake = new FakeSnowflake();
|
||||||
|
|
||||||
|
return await PerformImport(target, paths, dialog, callback, "Legacy Archive Import", async path => {
|
||||||
|
await using var jsonStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
|
return await LegacyArchiveImport.Read(jsonStream, target, fakeSnowflake, async servers => {
|
||||||
|
SynchronizationContext? prevSyncContext = SynchronizationContext.Current;
|
||||||
|
SynchronizationContext.SetSynchronizationContext(new AvaloniaSynchronizationContext());
|
||||||
|
Dictionary<DHT.Server.Data.Server, ulong>? result = await Dispatcher.UIThread.InvokeAsync(() => AskForServerIds(dialog, servers));
|
||||||
|
SynchronizationContext.SetSynchronizationContext(prevSyncContext);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Dictionary<DHT.Server.Data.Server, ulong>?> AskForServerIds(Window window, DHT.Server.Data.Server[] servers) {
|
||||||
|
static bool IsValidSnowflake(string value) {
|
||||||
|
return string.IsNullOrEmpty(value) || ulong.TryParse(value, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<TextBoxItem<DHT.Server.Data.Server>>();
|
||||||
|
|
||||||
|
foreach (DHT.Server.Data.Server server in servers.OrderBy(static server => server.Type).ThenBy(static server => server.Name)) {
|
||||||
|
items.Add(new TextBoxItem<DHT.Server.Data.Server>(server) {
|
||||||
|
Title = server.Name + " (" + ServerTypes.ToNiceString(server.Type) + ")",
|
||||||
|
ValidityCheck = IsValidSnowflake,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new TextBoxDialogModel<DHT.Server.Data.Server>(items) {
|
||||||
|
Title = "Imported Server IDs",
|
||||||
|
Description = "Please fill in the IDs of servers and direct messages. First enable Developer Mode in Discord, then right-click each server or direct message, click 'Copy ID', and paste it into the input field. If a server no longer exists, leave its input field empty to use a random ID.",
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = new TextBoxDialog { DataContext = model };
|
||||||
|
var result = await dialog.ShowDialog<DialogResult.OkCancel>(window);
|
||||||
|
|
||||||
|
if (result != DialogResult.OkCancel.Ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.ValidItems
|
||||||
|
.Where(static item => !string.IsNullOrEmpty(item.Value))
|
||||||
|
.ToDictionary(static item => item.Item, static item => ulong.Parse(item.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ImportResult?> PerformImport(IDatabaseFile target, string[] paths, ProgressDialog dialog, IProgressCallback callback, string dialogTitle, Func<string, Task<bool>> performImport) {
|
||||||
|
int total = paths.Length;
|
||||||
|
DatabaseStatistics oldStatistics = await DatabaseStatistics.Take(target);
|
||||||
|
|
||||||
|
int successful = 0;
|
||||||
|
int finished = 0;
|
||||||
|
|
||||||
|
foreach (string path in paths) {
|
||||||
|
await callback.Update(Path.GetFileName(path), finished, total);
|
||||||
|
++finished;
|
||||||
|
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
await Dialog.ShowOk(dialog, dialogTitle, "File '" + Path.GetFileName(path) + "' no longer exists.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await performImport(path)) {
|
||||||
|
++successful;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error("Could not import file: " + path, ex);
|
||||||
|
await Dialog.ShowOk(dialog, dialogTitle, "File '" + Path.GetFileName(path) + "' could not be imported: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await callback.Update("Done", finished, total);
|
||||||
|
|
||||||
|
if (successful == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseStatistics newStatistics = await DatabaseStatistics.Take(target);
|
||||||
|
return new ImportResult(oldStatistics, newStatistics, successful, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record DatabaseStatistics(long ServerCount, long ChannelCount, long UserCount, long MessageCount) {
|
||||||
|
public static async Task<DatabaseStatistics> Take(IDatabaseFile db) {
|
||||||
|
return new DatabaseStatistics(
|
||||||
|
await db.Servers.Count(),
|
||||||
|
await db.Channels.Count(),
|
||||||
|
await db.Users.Count(),
|
||||||
|
await db.Messages.Count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record ImportResult(DatabaseStatistics OldStatistics, DatabaseStatistics NewStatistics, int SuccessfulItems, int TotalItems);
|
||||||
|
|
||||||
|
private static string GetImportDialogMessage(ImportResult? result, string itemName) {
|
||||||
|
if (result == null) {
|
||||||
|
return "Nothing was imported.";
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldStatistics = result.OldStatistics;
|
||||||
|
var newStatistics = result.NewStatistics;
|
||||||
|
|
||||||
|
long newServers = newStatistics.ServerCount - oldStatistics.ServerCount;
|
||||||
|
long newChannels = newStatistics.ChannelCount - oldStatistics.ChannelCount;
|
||||||
|
long newUsers = newStatistics.UserCount - oldStatistics.UserCount;
|
||||||
|
long newMessages = newStatistics.MessageCount - oldStatistics.MessageCount;
|
||||||
|
|
||||||
|
var message = new StringBuilder();
|
||||||
|
message.Append("Processed ");
|
||||||
|
|
||||||
|
if (result.SuccessfulItems == result.TotalItems) {
|
||||||
|
message.Append(result.SuccessfulItems.Pluralize(itemName));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message.Append(result.SuccessfulItems.Format()).Append(" out of ").Append(result.TotalItems.Pluralize(itemName));
|
||||||
|
}
|
||||||
|
|
||||||
|
message.Append(" and added:\n\n \u2022 ");
|
||||||
|
message.Append(newServers.Pluralize("server")).Append("\n \u2022 ");
|
||||||
|
message.Append(newChannels.Pluralize("channel")).Append("\n \u2022 ");
|
||||||
|
message.Append(newUsers.Pluralize("user")).Append("\n \u2022 ");
|
||||||
|
message.Append(newMessages.Pluralize("message"));
|
||||||
|
|
||||||
|
return message.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task VacuumDatabase() {
|
||||||
|
const string Title = "Vacuum Database";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ProgressDialog.ShowIndeterminate(window, Title, "Vacuuming database...", _ => Db.Vacuum());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not vacuum database.", e);
|
||||||
|
await Dialog.ShowOk(window, Title, "Could not vacuum database: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Dialog.ShowOk(window, Title, "Done.");
|
||||||
|
}
|
||||||
|
}
|
52
app/Desktop/Main/Pages/DebugPage.axaml
Normal file
52
app/Desktop/Main/Pages/DebugPage.axaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<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.DebugPage"
|
||||||
|
x:DataType="pages:DebugPageModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:DebugPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="FontFamily" Value="Consolas,Courier" />
|
||||||
|
<Setter Property="FontSize" Value="15" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Label">
|
||||||
|
<Setter Property="Margin" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="WrapPanel > StackPanel">
|
||||||
|
<Setter Property="Orientation" Value="Vertical" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<Expander Header="Generate Random Data" IsExpanded="True">
|
||||||
|
<WrapPanel ItemSpacing="10" LineSpacing="10">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<Label Target="Channels">Channels</Label>
|
||||||
|
<TextBox x:Name="Channels" Width="100" Text="{Binding GenerateChannels}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<Label Target="Users">Users</Label>
|
||||||
|
<TextBox x:Name="Users" Width="100" Text="{Binding GenerateUsers}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<Label Target="Messages">Messages</Label>
|
||||||
|
<TextBox x:Name="Messages" Width="100" Text="{Binding GenerateMessages}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel VerticalAlignment="Bottom">
|
||||||
|
<Button Command="{Binding OnClickAddRandomDataToDatabase}">Add to Database</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</WrapPanel>
|
||||||
|
</Expander>
|
||||||
|
<Expander Header="About" IsExpanded="True">
|
||||||
|
<TextBlock Text="{Binding SqliteVersion, StringFormat=Sqlite Version: {0}}" />
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Pages/DebugPage.axaml.cs
Normal file
11
app/Desktop/Main/Pages/DebugPage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class DebugPage : UserControl {
|
||||||
|
public DebugPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
184
app/Desktop/Main/Pages/DebugPageModel.cs
Normal file
184
app/Desktop/Main/Pages/DebugPageModel.cs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#if DEBUG
|
||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Data;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed partial class DebugPageModel {
|
||||||
|
public string GenerateChannels { get; set; } = "0";
|
||||||
|
public string GenerateUsers { get; set; } = "0";
|
||||||
|
public string GenerateMessages { get; set; } = "0";
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private string? sqliteVersion = string.Empty;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly State state;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public DebugPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public DebugPageModel(Window window, State state) {
|
||||||
|
this.window = window;
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize() {
|
||||||
|
SqliteVersion = await state.Db.GetVersion() ?? "<unknown>";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OnClickAddRandomDataToDatabase() {
|
||||||
|
if (!int.TryParse(GenerateChannels, out int channels) || channels < 1) {
|
||||||
|
await Dialog.ShowOk(window, "Generate Random Data", "Amount of channels must be at least 1!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(GenerateUsers, out int users) || users < 1) {
|
||||||
|
await Dialog.ShowOk(window, "Generate Random Data", "Amount of users must be at least 1!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(GenerateMessages, out int messages) || messages < 1) {
|
||||||
|
await Dialog.ShowOk(window, "Generate Random Data", "Amount of messages must be at least 1!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProgressDialog.Show(window, "Generating Random Data", async (_, callback) => await GenerateRandomData(channels, users, messages, callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int BatchSize = 500;
|
||||||
|
|
||||||
|
private async Task GenerateRandomData(int channelCount, int userCount, int messageCount, IProgressCallback callback) {
|
||||||
|
int batchCount = (messageCount + BatchSize - 1) / BatchSize;
|
||||||
|
await callback.Update("Adding messages in batches of " + BatchSize, finishedItems: 0, totalItems: batchCount);
|
||||||
|
|
||||||
|
var rand = new Random();
|
||||||
|
var server = new DHT.Server.Data.Server {
|
||||||
|
Id = RandomId(rand),
|
||||||
|
Name = RandomName("s"),
|
||||||
|
Type = ServerType.Server,
|
||||||
|
};
|
||||||
|
|
||||||
|
Channel[] channels = Enumerable.Range(start: 0, channelCount).Select(i => new Channel {
|
||||||
|
Id = RandomId(rand),
|
||||||
|
Server = server.Id,
|
||||||
|
Name = RandomName("c"),
|
||||||
|
ParentId = null,
|
||||||
|
Position = i,
|
||||||
|
Topic = RandomText(rand, maxWords: 10),
|
||||||
|
Nsfw = rand.Next(4) == 0,
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
User[] users = Enumerable.Range(start: 0, userCount).Select(_ => new User {
|
||||||
|
Id = RandomId(rand),
|
||||||
|
Name = RandomName("u"),
|
||||||
|
DisplayName = RandomName("u"),
|
||||||
|
AvatarHash = null,
|
||||||
|
Discriminator = rand.Next(minValue: 0, maxValue: 9999).ToString(),
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
await state.Db.Users.Add(users);
|
||||||
|
await state.Db.Servers.Add([server]);
|
||||||
|
await state.Db.Channels.Add(channels);
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.Now;
|
||||||
|
int batchIndex = 0;
|
||||||
|
|
||||||
|
while (messageCount > 0) {
|
||||||
|
int hourOffset = batchIndex;
|
||||||
|
|
||||||
|
Message[] messages = Enumerable.Range(start: 0, Math.Min(messageCount, BatchSize)).Select(i => {
|
||||||
|
DateTimeOffset time = now.AddHours(hourOffset).AddMinutes(i * 60.0 / BatchSize);
|
||||||
|
DateTimeOffset? edit = rand.Next(100) == 0 ? time.AddSeconds(rand.Next(minValue: 1, maxValue: 60)) : null;
|
||||||
|
|
||||||
|
long timeMillis = time.ToUnixTimeMilliseconds();
|
||||||
|
long? editMillis = edit?.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
return new Message {
|
||||||
|
Id = (ulong) timeMillis,
|
||||||
|
Sender = RandomBiasedIndex(rand, users).Id,
|
||||||
|
Channel = RandomBiasedIndex(rand, channels).Id,
|
||||||
|
Text = RandomText(rand, maxWords: 100),
|
||||||
|
Timestamp = timeMillis,
|
||||||
|
EditTimestamp = editMillis,
|
||||||
|
RepliedToId = null,
|
||||||
|
Attachments = ImmutableList<Attachment>.Empty,
|
||||||
|
Embeds = ImmutableList<Embed>.Empty,
|
||||||
|
Reactions = ImmutableList<Reaction>.Empty,
|
||||||
|
};
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
await state.Db.Messages.Add(messages);
|
||||||
|
|
||||||
|
messageCount -= BatchSize;
|
||||||
|
await callback.Update("Adding messages in batches of " + BatchSize, ++batchIndex, batchCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong RandomId(Random rand) {
|
||||||
|
ulong h = unchecked((ulong) rand.Next());
|
||||||
|
ulong l = unchecked((ulong) rand.Next());
|
||||||
|
return (h << 32) | l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RandomName(string prefix) {
|
||||||
|
return prefix + "-" + ServerUtils.GenerateRandomToken(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T RandomBiasedIndex<T>(Random rand, T[] options) {
|
||||||
|
return options[(int) Math.Floor(options.Length * rand.NextDouble() * rand.NextDouble())];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] RandomWords = [
|
||||||
|
"apple", "apricot", "artichoke", "arugula", "asparagus", "avocado",
|
||||||
|
"banana", "bean", "beechnut", "beet", "blackberry", "blackcurrant", "blueberry", "boysenberry", "bramble", "broccoli",
|
||||||
|
"cabbage", "cacao", "cantaloupe", "caper", "carambola", "carrot", "cauliflower", "celery", "chard", "cherry", "chokeberry", "citron", "clementine", "coconut", "corn", "crabapple", "cranberry", "cucumber", "currant",
|
||||||
|
"daikon", "date", "dewberry", "durian",
|
||||||
|
"edamame", "eggplant", "elderberry", "endive",
|
||||||
|
"fig",
|
||||||
|
"garlic", "ginger", "gooseberry", "grape", "grapefruit", "guava",
|
||||||
|
"honeysuckle", "horseradish", "huckleberry",
|
||||||
|
"jackfruit", "jicama",
|
||||||
|
"kale", "kiwi", "kohlrabi", "kumquat",
|
||||||
|
"leek", "lemon", "lentil", "lettuce", "lime",
|
||||||
|
"mandarin", "mango", "mushroom", "myrtle",
|
||||||
|
"nectarine", "nut",
|
||||||
|
"olive", "okra", "onion", "orange",
|
||||||
|
"papaya", "parsnip", "pawpaw", "peach", "pear", "pea", "pepper", "persimmon", "pineapple", "plum", "plantain", "pomegranate", "pomelo", "potato", "prune", "pumpkin",
|
||||||
|
"quandong", "quinoa",
|
||||||
|
"radicchio", "radish", "raisin", "raspberry", "redcurrant", "rhubarb", "rutabaga",
|
||||||
|
"spinach", "strawberry", "squash",
|
||||||
|
"tamarind", "tangerine", "tomatillo", "tomato", "turnip",
|
||||||
|
"vanilla",
|
||||||
|
"watercress", "watermelon",
|
||||||
|
"yam",
|
||||||
|
"zucchini",
|
||||||
|
];
|
||||||
|
|
||||||
|
private static string RandomText(Random rand, int maxWords) {
|
||||||
|
int wordCount = 1 + (int) Math.Floor(maxWords * Math.Pow(rand.NextDouble(), y: 3));
|
||||||
|
return string.Join(separator: ' ', Enumerable.Range(start: 0, wordCount).Select(_ => RandomWords[rand.Next(RandomWords.Length)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed class DebugPageModel {
|
||||||
|
public string GenerateChannels { get; set; } = "0";
|
||||||
|
public string GenerateUsers { get; set; } = "0";
|
||||||
|
public string GenerateMessages { get; set; } = "0";
|
||||||
|
|
||||||
|
public string SqliteVersion => string.Empty;
|
||||||
|
|
||||||
|
public void OnClickAddRandomDataToDatabase() {}
|
||||||
|
}
|
||||||
|
#endif
|
51
app/Desktop/Main/Pages/DownloadsPage.axaml
Normal file
51
app/Desktop/Main/Pages/DownloadsPage.axaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<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"
|
||||||
|
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.Main.Pages.DownloadsPage"
|
||||||
|
x:DataType="pages:DownloadsPageModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:DownloadsPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="DataGridColumnHeader">
|
||||||
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridColumnHeader:nth-child(2)">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridColumnHeader:nth-child(3)">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridCell.right">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="20">
|
||||||
|
<WrapPanel Orientation="Horizontal" ItemSpacing="10" LineSpacing="10">
|
||||||
|
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" />
|
||||||
|
<Button Command="{Binding OnClickRetryFailed}" IsEnabled="{Binding IsRetryFailedOnDownloadsButtonEnabled}">Retry Failed</Button>
|
||||||
|
<Button Command="{Binding OnClickDeleteOrphaned}">Delete Orphaned</Button>
|
||||||
|
<Button Command="{Binding OnClickExportAll}" IsEnabled="{Binding HasSuccessfulDownloads}">Export All...</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
<controls:DownloadItemFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !$parent[UserControl].((pages:DownloadsPageModel)DataContext).IsDownloading}" />
|
||||||
|
<TextBlock TextWrapping="Wrap">
|
||||||
|
Downloading state and filter settings are remembered per-database.
|
||||||
|
</TextBlock>
|
||||||
|
<Expander Header="Download Status" IsExpanded="True">
|
||||||
|
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="State" Binding="{Binding State, Mode=OneWay}" Width="*" />
|
||||||
|
<DataGridTextColumn Header="Files" Binding="{Binding Items, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||||
|
<DataGridTextColumn Header="Size" Binding="{Binding SizeText, Mode=OneWay}" Width="*" CellStyleClasses="right" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Pages/DownloadsPage.axaml.cs
Normal file
11
app/Desktop/Main/Pages/DownloadsPage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class DownloadsPage : UserControl {
|
||||||
|
public DownloadsPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
336
app/Desktop/Main/Pages/DownloadsPageModel.cs
Normal file
336
app/Desktop/Main/Pages/DownloadsPageModel.cs
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Desktop.Dialogs.File;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Desktop.Main.Controls;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Data;
|
||||||
|
using DHT.Server.Data.Aggregations;
|
||||||
|
using DHT.Server.Data.Filters;
|
||||||
|
using DHT.Server.Data.Settings;
|
||||||
|
using DHT.Server.Download;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using DHT.Utils.Tasks;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed partial class DownloadsPageModel : IAsyncDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<DownloadsPageModel>();
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool isToggleDownloadButtonEnabled = true;
|
||||||
|
|
||||||
|
[DependsOn(nameof(IsDownloading))]
|
||||||
|
public string ToggleDownloadButtonText => IsDownloading ? "Stop Downloading" : "Start Downloading";
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool isRetryingFailedDownloads = false;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool hasSuccessfulDownloads;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool hasFailedDownloads;
|
||||||
|
|
||||||
|
[DependsOn(nameof(IsRetryingFailedDownloads), nameof(HasFailedDownloads))]
|
||||||
|
public bool IsRetryFailedOnDownloadsButtonEnabled => !IsRetryingFailedDownloads && HasFailedDownloads;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private string downloadMessage = "";
|
||||||
|
|
||||||
|
public DownloadItemFilterPanelModel FilterModel { get; }
|
||||||
|
|
||||||
|
private readonly StatisticsRow statisticsPending = new ("Pending");
|
||||||
|
private readonly StatisticsRow statisticsDownloaded = new ("Downloaded");
|
||||||
|
private readonly StatisticsRow statisticsFailed = new ("Failed");
|
||||||
|
private readonly StatisticsRow statisticsSkipped = new ("Skipped");
|
||||||
|
|
||||||
|
public ObservableCollection<StatisticsRow> StatisticsRows { get; }
|
||||||
|
|
||||||
|
public bool IsDownloading => state.Downloader.IsDownloading;
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly State state;
|
||||||
|
private readonly ThrottledTask<DownloadStatusStatistics> downloadStatisticsTask;
|
||||||
|
private readonly IDisposable downloadItemCountSubscription;
|
||||||
|
|
||||||
|
private IDisposable? finishedItemsSubscription;
|
||||||
|
private DownloadItemFilter? currentDownloadFilter;
|
||||||
|
|
||||||
|
public DownloadsPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public DownloadsPageModel(Window window, State state) {
|
||||||
|
this.window = window;
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
FilterModel = new DownloadItemFilterPanelModel(state);
|
||||||
|
|
||||||
|
StatisticsRows = [
|
||||||
|
statisticsPending,
|
||||||
|
statisticsDownloaded,
|
||||||
|
statisticsFailed,
|
||||||
|
statisticsSkipped,
|
||||||
|
];
|
||||||
|
|
||||||
|
downloadStatisticsTask = new ThrottledTask<DownloadStatusStatistics>(Log, UpdateStatistics, TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
|
downloadItemCountSubscription = state.Db.Downloads.TotalCount.ObserveOn(AvaloniaScheduler.Instance).Subscribe(OnDownloadCountChanged);
|
||||||
|
|
||||||
|
RecomputeDownloadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize() {
|
||||||
|
await FilterModel.Initialize();
|
||||||
|
|
||||||
|
if (await state.Db.Settings.Get(SettingsKey.DownloadsAutoStart, defaultValue: false)) {
|
||||||
|
try {
|
||||||
|
await StartDownload();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not automatically start downloads.", e);
|
||||||
|
await Dialog.ShowOk(window, "Database Error", "Could not automatically start downloads: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
finishedItemsSubscription?.Dispose();
|
||||||
|
|
||||||
|
downloadItemCountSubscription.Dispose();
|
||||||
|
downloadStatisticsTask.Dispose();
|
||||||
|
|
||||||
|
await FilterModel.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDownloadCountChanged(long newDownloadCount) {
|
||||||
|
RecomputeDownloadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickToggleDownload() {
|
||||||
|
IsToggleDownloadButtonEnabled = false;
|
||||||
|
try {
|
||||||
|
if (IsDownloading) {
|
||||||
|
await StopDownload();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
await StartDownload();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not start downloads.", e);
|
||||||
|
await Dialog.ShowOk(window, "Database Error", "Could not start downloads: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await state.Db.Settings.Set(SettingsKey.DownloadsAutoStart, IsDownloading);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not update auto-start setting in database.", e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
IsToggleDownloadButtonEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartDownload() {
|
||||||
|
await state.Db.Downloads.MoveDownloadingItemsBackToQueue();
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentDownloadFilter = FilterModel.CreateFilter();
|
||||||
|
IObservable<DownloadItem> finishedItems = await state.Downloader.Start(currentDownloadFilter);
|
||||||
|
finishedItemsSubscription = finishedItems.ObserveOn(AvaloniaScheduler.Instance).Subscribe(OnItemFinished);
|
||||||
|
} catch (Exception) {
|
||||||
|
finishedItemsSubscription?.Dispose();
|
||||||
|
finishedItemsSubscription = null;
|
||||||
|
currentDownloadFilter = null;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnDownloadStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopDownload() {
|
||||||
|
await state.Downloader.Stop();
|
||||||
|
await state.Db.Downloads.MoveDownloadingItemsBackToQueue();
|
||||||
|
|
||||||
|
finishedItemsSubscription?.Dispose();
|
||||||
|
finishedItemsSubscription = null;
|
||||||
|
currentDownloadFilter = null;
|
||||||
|
|
||||||
|
OnDownloadStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDownloadStateChanged() {
|
||||||
|
RecomputeDownloadStatistics();
|
||||||
|
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsDownloading)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemFinished(DownloadItem item) {
|
||||||
|
RecomputeDownloadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickRetryFailed() {
|
||||||
|
IsRetryingFailedDownloads = true;
|
||||||
|
try {
|
||||||
|
await state.Db.Downloads.RetryFailed();
|
||||||
|
RecomputeDownloadStatistics();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not retry failed downloads.", e);
|
||||||
|
await Dialog.ShowOk(window, "Retry Failed", "Could not retry failed downloads: " + e.Message);
|
||||||
|
} finally {
|
||||||
|
IsRetryingFailedDownloads = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeDownloadStatistics() {
|
||||||
|
downloadStatisticsTask.Post(cancellationToken => state.Db.Downloads.GetStatistics(currentDownloadFilter ?? new DownloadItemFilter(), cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickDeleteOrphaned() {
|
||||||
|
const string Title = "Delete Orphaned Downloads";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ProgressDialog.Show(window, Title, async (_, callback) => {
|
||||||
|
await callback.UpdateIndeterminate("Searching for orphaned downloads...");
|
||||||
|
|
||||||
|
HashSet<string> reachableNormalizedUrls = [];
|
||||||
|
HashSet<string> orphanedNormalizedUrls = [];
|
||||||
|
|
||||||
|
await foreach (FileUrl fileUrl in state.Db.Downloads.FindReachableFiles()) {
|
||||||
|
reachableNormalizedUrls.Add(fileUrl.NormalizedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await foreach (Download download in state.Db.Downloads.Get()) {
|
||||||
|
string normalizedUrl = download.NormalizedUrl;
|
||||||
|
if (!reachableNormalizedUrls.Contains(normalizedUrl)) {
|
||||||
|
orphanedNormalizedUrls.Add(normalizedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orphanedNormalizedUrls.Count == 0) {
|
||||||
|
await Dialog.ShowOk(window, Title, "No orphaned downloads found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await Dialog.ShowYesNo(window, Title, orphanedNormalizedUrls.Count + " orphaned download(s) will be removed from this database. This action cannot be undone. Proceed?") != DialogResult.YesNo.Yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await callback.UpdateIndeterminate("Deleting orphaned downloads...");
|
||||||
|
await state.Db.Downloads.Remove(orphanedNormalizedUrls);
|
||||||
|
RecomputeDownloadStatistics();
|
||||||
|
|
||||||
|
if (await Dialog.ShowYesNo(window, Title, "Orphaned downloads deleted. Vacuum database now to reclaim space?") != DialogResult.YesNo.Yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await callback.UpdateIndeterminate("Vacuuming database...");
|
||||||
|
await state.Db.Vacuum();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not delete orphaned downloads.", e);
|
||||||
|
await Dialog.ShowOk(window, Title, "Could not delete orphaned downloads: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickExportAll() {
|
||||||
|
const string Title = "Export Downloaded Files";
|
||||||
|
|
||||||
|
string[] folders = await window.StorageProvider.OpenFolders(new FolderPickerOpenOptions {
|
||||||
|
Title = Title,
|
||||||
|
AllowMultiple = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folders.Length != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string folderPath = folders[0];
|
||||||
|
|
||||||
|
DownloadExporter exporter = new DownloadExporter(state.Db, folderPath);
|
||||||
|
DownloadExporter.Result result;
|
||||||
|
try {
|
||||||
|
result = await ProgressDialog.Show(window, Title, async (_, callback) => {
|
||||||
|
await callback.UpdateIndeterminate("Exporting downloaded files...");
|
||||||
|
return await exporter.Export(new ExportProgressReporter(callback));
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not export downloaded files.", e);
|
||||||
|
await Dialog.ShowOk(window, Title, "Could not export downloaded files: " + e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string messageStart = "Exported " + result.SuccessfulCount.Pluralize("file");
|
||||||
|
|
||||||
|
if (result.FailedCount > 0L) {
|
||||||
|
await Dialog.ShowOk(window, Title, messageStart + " (" + result.FailedCount.Format() + " failed).");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await Dialog.ShowOk(window, Title, messageStart + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ExportProgressReporter(IProgressCallback callback) : DownloadExporter.IProgressReporter {
|
||||||
|
public Task ReportProgress(long processedCount, long totalCount) {
|
||||||
|
return callback.Update("Exporting downloaded files...", processedCount, totalCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStatistics(DownloadStatusStatistics statusStatistics) {
|
||||||
|
statisticsPending.Items = statusStatistics.PendingCount;
|
||||||
|
statisticsPending.Size = statusStatistics.PendingTotalSize;
|
||||||
|
statisticsPending.HasFilesWithUnknownSize = statusStatistics.PendingWithUnknownSizeCount > 0;
|
||||||
|
|
||||||
|
statisticsDownloaded.Items = statusStatistics.SuccessfulCount;
|
||||||
|
statisticsDownloaded.Size = statusStatistics.SuccessfulTotalSize;
|
||||||
|
statisticsDownloaded.HasFilesWithUnknownSize = statusStatistics.SuccessfulWithUnknownSizeCount > 0;
|
||||||
|
|
||||||
|
statisticsFailed.Items = statusStatistics.FailedCount;
|
||||||
|
statisticsFailed.Size = statusStatistics.FailedTotalSize;
|
||||||
|
statisticsFailed.HasFilesWithUnknownSize = statusStatistics.FailedWithUnknownSizeCount > 0;
|
||||||
|
|
||||||
|
statisticsSkipped.Items = statusStatistics.SkippedCount;
|
||||||
|
statisticsSkipped.Size = statusStatistics.SkippedTotalSize;
|
||||||
|
statisticsSkipped.HasFilesWithUnknownSize = statusStatistics.SkippedWithUnknownSizeCount > 0;
|
||||||
|
|
||||||
|
HasSuccessfulDownloads = statusStatistics.SuccessfulCount > 0;
|
||||||
|
HasFailedDownloads = statusStatistics.FailedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed partial class StatisticsRow(string state) {
|
||||||
|
public string State { get; } = state;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private int items;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private ulong? size;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool hasFilesWithUnknownSize;
|
||||||
|
|
||||||
|
[DependsOn(nameof(Size), nameof(HasFilesWithUnknownSize))]
|
||||||
|
public string SizeText {
|
||||||
|
get {
|
||||||
|
if (size == null) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
else if (hasFilesWithUnknownSize) {
|
||||||
|
return "\u2265 " + BytesValueConverter.Convert(size.Value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return BytesValueConverter.Convert(size.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
app/Desktop/Main/Pages/TrackingPage.axaml
Normal file
54
app/Desktop/Main/Pages/TrackingPage.axaml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<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"
|
||||||
|
x:DataType="pages:TrackingPageModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:TrackingPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<StackPanel Spacing="20">
|
||||||
|
<Expander Header="Method 1: Manual" IsExpanded="True">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<TextBlock>
|
||||||
|
<TextBlock.Text>
|
||||||
|
<MultiBinding StringFormat="Use {0} to open Dev Tools in your browser or the Discord app, copy the tracking script, and paste it into the console.">
|
||||||
|
<Binding Path="OpenDevToolsShortcutText" />
|
||||||
|
</MultiBinding>
|
||||||
|
</TextBlock.Text>
|
||||||
|
</TextBlock>
|
||||||
|
<Button x:Name="CopyTrackingScript" Click="CopyTrackingScriptButton_OnClick">Copy Tracking Script</Button>
|
||||||
|
<TextBlock Margin="0 5 0 0">
|
||||||
|
By default, the Discord app blocks the Dev Tools shortcut. The button below changes a hidden setting to unblock the shortcut.
|
||||||
|
</TextBlock>
|
||||||
|
<Button Command="{Binding OnClickToggleAppDevTools}" Content="{Binding ToggleAppDevToolsButtonText}" IsEnabled="{Binding IsToggleAppDevToolsButtonEnabled}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
<Expander Header="Method 2: Userscript" IsExpanded="True">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<TextBlock>
|
||||||
|
Requires a userscript manager in your browser. The userscript adds a DHT icon next to the Help icon on Discord.
|
||||||
|
If the icon does not appear, update this app and reinstall the userscript.
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock>
|
||||||
|
Copy the Connection Code, click the DHT icon, and paste the code into the prompt.
|
||||||
|
</TextBlock>
|
||||||
|
<WrapPanel ItemSpacing="10" LineSpacing="10">
|
||||||
|
<Button Command="{Binding OnClickInstallOrUpdateUserscript}">Install or Update Userscript</Button>
|
||||||
|
<Button x:Name="CopyConnectionCode" Click="CopyConnectionScriptButton_OnClick">Copy Connection Code</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
45
app/Desktop/Main/Pages/TrackingPage.axaml.cs
Normal file
45
app/Desktop/Main/Pages/TrackingPage.axaml.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class TrackingPage : UserControl {
|
||||||
|
private readonly HashSet<Button> copyingButtons = new (ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
|
public TrackingPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void CopyTrackingScriptButton_OnClick(object? sender, RoutedEventArgs e) {
|
||||||
|
await HandleCopyButton(CopyTrackingScript, "Script Copied!", static model => model.OnClickCopyTrackingScript());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void CopyConnectionScriptButton_OnClick(object? sender, RoutedEventArgs e) {
|
||||||
|
await HandleCopyButton(CopyConnectionCode, "Code Copied!", static model => model.OnClickCopyConnectionCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCopyButton(Button button, string copiedText, Func<TrackingPageModel, Task<bool>> onClick) {
|
||||||
|
if (DataContext is TrackingPageModel model) {
|
||||||
|
object? originalText = button.Content;
|
||||||
|
button.MinWidth = button.Bounds.Width;
|
||||||
|
|
||||||
|
if (await onClick(model) && copyingButtons.Add(button)) {
|
||||||
|
button.IsEnabled = false;
|
||||||
|
button.Content = copiedText;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||||
|
} finally {
|
||||||
|
copyingButtons.Remove(button);
|
||||||
|
button.IsEnabled = true;
|
||||||
|
button.Content = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
app/Desktop/Main/Pages/TrackingPageModel.cs
Normal file
154
app/Desktop/Main/Pages/TrackingPageModel.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Discord;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using static DHT.Desktop.Program;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed partial class TrackingPageModel {
|
||||||
|
private static readonly Log Log = Log.ForType<TrackingPageModel>();
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool? areDevToolsEnabled = null;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)]
|
||||||
|
private bool isToggleAppDevToolsButtonEnabled = false;
|
||||||
|
|
||||||
|
public string OpenDevToolsShortcutText { get; } = OperatingSystem.IsMacOS() ? "Cmd+Shift+I" : "Ctrl+Shift+I";
|
||||||
|
|
||||||
|
[DependsOn(nameof(AreDevToolsEnabled), nameof(IsToggleAppDevToolsButtonEnabled))]
|
||||||
|
public string ToggleAppDevToolsButtonText {
|
||||||
|
get {
|
||||||
|
if (!AreDevToolsEnabled.HasValue) {
|
||||||
|
return "Loading...";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsToggleAppDevToolsButtonEnabled) {
|
||||||
|
return "Unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (AreDevToolsEnabled.Value ? "Disable" : "Enable") + " " + OpenDevToolsShortcutText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public TrackingPageModel() : this(null!) {}
|
||||||
|
|
||||||
|
public TrackingPageModel(Window window) {
|
||||||
|
this.window = window;
|
||||||
|
|
||||||
|
Task.Factory.StartNew(InitializeDevToolsToggle, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> OnClickCopyTrackingScript() {
|
||||||
|
string url = ServerConfiguration.HttpHost + $"/get-tracking-script?token={HttpUtility.UrlEncode(ServerConfiguration.Token)}";
|
||||||
|
string script = (await Resources.ReadTextAsync("tracker-loader.js")).Trim().Replace("{url}", url);
|
||||||
|
return await TryCopy(script, "Copy Tracking Script");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeDevToolsToggle() {
|
||||||
|
bool? devToolsEnabled = await Task.Run(DiscordAppSettings.AreDevToolsEnabled);
|
||||||
|
|
||||||
|
if (devToolsEnabled.HasValue) {
|
||||||
|
AreDevToolsEnabled = devToolsEnabled.Value;
|
||||||
|
IsToggleAppDevToolsButtonEnabled = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
IsToggleAppDevToolsButtonEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickToggleAppDevTools() {
|
||||||
|
const string DialogTitle = "Discord App Settings File";
|
||||||
|
|
||||||
|
if (!AreDevToolsEnabled.HasValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool oldState = AreDevToolsEnabled.Value;
|
||||||
|
bool newState = !oldState;
|
||||||
|
|
||||||
|
switch (await DiscordAppSettings.ConfigureDevTools(newState)) {
|
||||||
|
case SettingsJsonResult.Success:
|
||||||
|
AreDevToolsEnabled = newState;
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, OpenDevToolsShortcutText + " was " + (newState ? "enabled." : "disabled.") + " Restart the Discord app for the change to take effect.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingsJsonResult.AlreadySet:
|
||||||
|
await Dialog.ShowOk(window, DialogTitle, OpenDevToolsShortcutText + " 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickInstallOrUpdateUserscript() {
|
||||||
|
try {
|
||||||
|
SystemUtils.OpenUrl(ServerConfiguration.HttpHost + "/get-userscript/dht.user.js");
|
||||||
|
} catch (Exception e) {
|
||||||
|
await Dialog.ShowOk(window, "Install or Update Userscript", "Could not open the browser: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("^[a-zA-Z0-9]{1,100}$")]
|
||||||
|
private static partial Regex ConnectionCodeTokenRegex();
|
||||||
|
|
||||||
|
public async Task<bool> OnClickCopyConnectionCode() {
|
||||||
|
const string Title = "Copy Connection Code";
|
||||||
|
|
||||||
|
if (ConnectionCodeTokenRegex().IsMatch(ServerConfiguration.Token)) {
|
||||||
|
return await TryCopy(ServerConfiguration.Port + ":" + ServerConfiguration.Token, Title);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await Dialog.ShowOk(window, Title, "The internal server token cannot be used to create a connection code.\n\nCheck the 'Advanced' tab and ensure the token is 1-100 characters long, and only contains plain letters and numbers.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TryCopy(string script, string errorDialogTitle) {
|
||||||
|
IClipboard? clipboard = window.Clipboard;
|
||||||
|
if (clipboard == null) {
|
||||||
|
await Dialog.ShowOk(window, errorDialogTitle, "Clipboard is not available on this system.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await clipboard.SetTextAsync(script);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not copy to clipboard.", e);
|
||||||
|
await Dialog.ShowOk(window, errorDialogTitle, "An error occurred while copying to clipboard.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
app/Desktop/Main/Pages/ViewerPage.axaml
Normal file
37
app/Desktop/Main/Pages/ViewerPage.axaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<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"
|
||||||
|
xmlns:controls="clr-namespace:DHT.Desktop.Main.Controls"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.Main.Pages.ViewerPage"
|
||||||
|
x:DataType="pages:ViewerPageModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<pages:ViewerPageModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Expander">
|
||||||
|
<Setter Property="Margin" Value="0 5 0 0" />
|
||||||
|
</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>
|
||||||
|
</StackPanel>
|
||||||
|
<controls:MessageFilterPanel DataContext="{Binding FilterModel}" />
|
||||||
|
<Expander Header="Database Tools">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="10">
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="4">
|
||||||
|
<RadioButton GroupName="DatabaseToolFilterMode" IsEnabled="{Binding HasFilters}" IsChecked="{Binding DatabaseToolFilterModeKeep}">Keep Only Messages Matching Filters</RadioButton>
|
||||||
|
<RadioButton GroupName="DatabaseToolFilterMode" IsEnabled="{Binding HasFilters}" IsChecked="{Binding DatabaseToolFilterModeRemove}">Remove Messages Matching Filters</RadioButton>
|
||||||
|
</StackPanel>
|
||||||
|
<Button IsEnabled="{Binding HasFilters}" Command="{Binding OnClickApplyFiltersToDatabase}">Apply Filters to Database</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Expander>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Pages/ViewerPage.axaml.cs
Normal file
11
app/Desktop/Main/Pages/ViewerPage.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class ViewerPage : UserControl {
|
||||||
|
public ViewerPage() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
101
app/Desktop/Main/Pages/ViewerPageModel.cs
Normal file
101
app/Desktop/Main/Pages/ViewerPageModel.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.Common;
|
||||||
|
using DHT.Desktop.Dialogs.Message;
|
||||||
|
using DHT.Desktop.Dialogs.Progress;
|
||||||
|
using DHT.Desktop.Main.Controls;
|
||||||
|
using DHT.Desktop.Server;
|
||||||
|
using DHT.Server;
|
||||||
|
using DHT.Server.Data.Filters;
|
||||||
|
using DHT.Server.Service.Viewer;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Pages;
|
||||||
|
|
||||||
|
sealed partial class ViewerPageModel : IDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<ViewerPageModel>();
|
||||||
|
|
||||||
|
public bool DatabaseToolFilterModeKeep { get; set; } = true;
|
||||||
|
public bool DatabaseToolFilterModeRemove { get; set; } = false;
|
||||||
|
|
||||||
|
[Notify]
|
||||||
|
private bool hasFilters = false;
|
||||||
|
|
||||||
|
public MessageFilterPanelModel FilterModel { get; }
|
||||||
|
|
||||||
|
private readonly Window window;
|
||||||
|
private readonly State state;
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public ViewerPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public ViewerPageModel(Window window, State state) {
|
||||||
|
this.window = window;
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
FilterModel = new MessageFilterPanelModel(window, state, "Will export");
|
||||||
|
FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
FilterModel.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFilterPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
HasFilters = FilterModel.HasAnyFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void OnClickOpenViewer() {
|
||||||
|
try {
|
||||||
|
string serverToken = ServerConfiguration.Token;
|
||||||
|
string sessionId = state.ViewerSessions.Register(new ViewerSession(FilterModel.CreateFilter())).ToString();
|
||||||
|
SystemUtils.OpenUrl(ServerConfiguration.HttpHost + "/viewer/?token=" + HttpUtility.UrlEncode(serverToken) + "&session=" + HttpUtility.UrlEncode(sessionId));
|
||||||
|
} catch (Exception e) {
|
||||||
|
await Dialog.ShowOk(window, "Open Viewer", "Could not open viewer: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnClickApplyFiltersToDatabase() {
|
||||||
|
try {
|
||||||
|
MessageFilter filter = FilterModel.CreateFilter();
|
||||||
|
long messageCount = await ProgressDialog.ShowIndeterminate(window, "Apply Filters", "Counting matching messages...", _ => state.Db.Messages.Count(filter));
|
||||||
|
|
||||||
|
if (DatabaseToolFilterModeKeep) {
|
||||||
|
if (DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Keep Matching Messages in This Database", messageCount.Pluralize("message") + " will be kept, and the rest will be removed from this database. This action cannot be undone. Proceed?")) {
|
||||||
|
await ApplyFilterToDatabase(filter, FilterRemovalMode.KeepMatching);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (DatabaseToolFilterModeRemove) {
|
||||||
|
if (DialogResult.YesNo.Yes == await Dialog.ShowYesNo(window, "Remove Matching Messages in This Database", messageCount.Pluralize("message") + " will be removed from this database. This action cannot be undone. Proceed?")) {
|
||||||
|
await ApplyFilterToDatabase(filter, FilterRemovalMode.RemoveMatching);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error("Could not apply filters.", e);
|
||||||
|
await Dialog.ShowOk(window, "Apply Filters", "Could not apply filters: " + e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyFilterToDatabase(MessageFilter filter, FilterRemovalMode removalMode) {
|
||||||
|
await ProgressDialog.Show(window, "Apply Filters", async (_, callback) => {
|
||||||
|
await callback.UpdateIndeterminate("Removing messages...");
|
||||||
|
Log.Info("Removed messages: " + await state.Db.Messages.Remove(filter, removalMode));
|
||||||
|
|
||||||
|
await callback.UpdateIndeterminate("Cleaning up attachments...");
|
||||||
|
Log.Info("Removed orphaned attachments: " + await state.Db.Messages.RemoveUnreachableAttachments());
|
||||||
|
|
||||||
|
await callback.UpdateIndeterminate("Cleaning up users...");
|
||||||
|
Log.Info("Removed orphaned users: " + await state.Db.Users.RemoveUnreachable());
|
||||||
|
|
||||||
|
await callback.UpdateIndeterminate("Cleaning up channels...");
|
||||||
|
Log.Info("Removed orphaned channels: " + await state.Db.Channels.RemoveUnreachable());
|
||||||
|
|
||||||
|
await callback.UpdateIndeterminate("Cleaning up servers...");
|
||||||
|
Log.Info("Removed orphaned servers: " + await state.Db.Servers.RemoveUnreachable());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
122
app/Desktop/Main/Screens/MainContentScreen.axaml
Normal file
122
app/Desktop/Main/Screens/MainContentScreen.axaml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<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.Main.Controls"
|
||||||
|
xmlns:screens="clr-namespace:DHT.Desktop.Main.Screens"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="DHT.Desktop.Main.Screens.MainContentScreen"
|
||||||
|
x:DataType="screens:MainContentScreenModel">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<screens:MainContentScreenModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<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" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Foreground" Value="#E9E9E9" />
|
||||||
|
<Setter Property="FontSize" Value="20" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem[TabStripPlacement=Left] /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Margin" Value="5 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:pointerover">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Background" Value="#455785" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:pointerover /template/ Border">
|
||||||
|
<Setter Property="Background" Value="#455785" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Foreground" Value="#E9E9E9" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:selected:pointerover /template/ Border">
|
||||||
|
<Setter Property="Background" Value="#FFFFFF" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:selected:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Foreground" Value="#1A2234" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:selected">
|
||||||
|
<Setter Property="Foreground" Value="#1A2234" />
|
||||||
|
<Setter Property="Background" Value="#FFFFFF" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:selected /template/ Border#PART_SelectedPipe">
|
||||||
|
<Setter Property="IsVisible" Value="False" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:disabled /template/ ContentPresenter">
|
||||||
|
<Setter Property="Foreground" Value="#B2B2B2" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem:disabled /template/ ContentPresenter">
|
||||||
|
<Setter Property="AccessText.TextDecorations" Value="Strikethrough" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabItem.first">
|
||||||
|
<Setter Property="Margin" Value="0 13 0 0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TabControl">
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ContentPresenter.page">
|
||||||
|
<Setter Property="Margin" Value="15 21 20 21" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<DockPanel>
|
||||||
|
<Border Classes="statusBar" DockPanel.Dock="Bottom">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock Classes="invisibleTabItem" DockPanel.Dock="Left">Downloads</TextBlock>
|
||||||
|
<controls:StatusBar DataContext="{Binding StatusBarModel}" DockPanel.Dock="Right" />
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<TabControl x:Name="TabControl" TabStripPlacement="Left" DockPanel.Dock="Top">
|
||||||
|
<TabControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Grid ColumnDefinitions="Auto" RowDefinitions="Auto,Auto,Auto,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 x:Name="TabTracking" Header="Tracking" Grid.Row="1">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ContentPresenter Content="{Binding TrackingPage}" Classes="page" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem x:Name="TabDownloads" Header="Downloads" Grid.Row="2">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ContentPresenter Content="{Binding DownloadsPage}" Classes="page" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem x:Name="TabViewer" Header="Viewer" Grid.Row="3">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ContentPresenter Content="{Binding ViewerPage}" Classes="page" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem x:Name="TabAdvanced" Header="Advanced" Grid.Row="5">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ContentPresenter Content="{Binding AdvancedPage}" Classes="page" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem x:Name="TabDebug" Header="Debug" Grid.Row="6" IsVisible="{Binding HasDebugPage}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ContentPresenter Content="{Binding DebugPage}" Classes="page" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
</UserControl>
|
11
app/Desktop/Main/Screens/MainContentScreen.axaml.cs
Normal file
11
app/Desktop/Main/Screens/MainContentScreen.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Screens;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
|
public sealed partial class MainContentScreen : UserControl {
|
||||||
|
public MainContentScreen() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
85
app/Desktop/Main/Screens/MainContentScreenModel.cs
Normal file
85
app/Desktop/Main/Screens/MainContentScreenModel.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using DHT.Desktop.Main.Controls;
|
||||||
|
using DHT.Desktop.Main.Pages;
|
||||||
|
using DHT.Server;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Main.Screens;
|
||||||
|
|
||||||
|
sealed class MainContentScreenModel : IAsyncDisposable {
|
||||||
|
public DatabasePage DatabasePage { get; }
|
||||||
|
private DatabasePageModel DatabasePageModel { get; }
|
||||||
|
|
||||||
|
public TrackingPage TrackingPage { get; }
|
||||||
|
private TrackingPageModel TrackingPageModel { get; }
|
||||||
|
|
||||||
|
public DownloadsPage DownloadsPage { get; }
|
||||||
|
private DownloadsPageModel DownloadsPageModel { get; }
|
||||||
|
|
||||||
|
public ViewerPage ViewerPage { get; }
|
||||||
|
private ViewerPageModel ViewerPageModel { get; }
|
||||||
|
|
||||||
|
public AdvancedPage AdvancedPage { get; }
|
||||||
|
private AdvancedPageModel AdvancedPageModel { get; }
|
||||||
|
|
||||||
|
public DebugPage? DebugPage { get; }
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
public bool HasDebugPage => true;
|
||||||
|
private DebugPageModel DebugPageModel { get; }
|
||||||
|
#else
|
||||||
|
public bool HasDebugPage => false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public StatusBarModel StatusBarModel { get; }
|
||||||
|
|
||||||
|
public event EventHandler? DatabaseClosed {
|
||||||
|
add => DatabasePageModel.DatabaseClosed += value;
|
||||||
|
remove => DatabasePageModel.DatabaseClosed -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Designer")]
|
||||||
|
public MainContentScreenModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
|
public MainContentScreenModel(Window window, State state) {
|
||||||
|
DatabasePageModel = new DatabasePageModel(window, state);
|
||||||
|
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
||||||
|
|
||||||
|
TrackingPageModel = new TrackingPageModel(window);
|
||||||
|
TrackingPage = new TrackingPage { DataContext = TrackingPageModel };
|
||||||
|
|
||||||
|
DownloadsPageModel = new DownloadsPageModel(window, state);
|
||||||
|
DownloadsPage = new DownloadsPage { DataContext = DownloadsPageModel };
|
||||||
|
|
||||||
|
ViewerPageModel = new ViewerPageModel(window, state);
|
||||||
|
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
||||||
|
|
||||||
|
AdvancedPageModel = new AdvancedPageModel(window, state);
|
||||||
|
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
DebugPageModel = new DebugPageModel(window, state);
|
||||||
|
DebugPage = new DebugPage { DataContext = DebugPageModel };
|
||||||
|
#else
|
||||||
|
DebugPage = null;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
StatusBarModel = new StatusBarModel(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize() {
|
||||||
|
await DownloadsPageModel.Initialize();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
await DebugPageModel.Initialize();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
await DownloadsPageModel.DisposeAsync();
|
||||||
|
ViewerPageModel.Dispose();
|
||||||
|
AdvancedPageModel.Dispose();
|
||||||
|
StatusBarModel.Dispose();
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user