Compare commits
1298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91db21067c | ||
|
|
834546e3ba | ||
|
|
6518fe6946 | ||
|
|
9b21193be2 | ||
|
|
99983b1eb1 | ||
|
|
712e2c9d02 | ||
|
|
f963e6aa03 | ||
|
|
d3c25e6b12 | ||
|
|
667abf2131 | ||
|
|
8a1e17ecd3 | ||
|
|
809004e16b | ||
|
|
e35df8a029 | ||
|
|
d9081563c2 | ||
|
|
29e757ba75 | ||
|
|
48724bae8b | ||
|
|
b1de430f0b | ||
|
|
6cd18ba7f7 | ||
|
|
07d167d961 | ||
|
|
dadedbe129 | ||
|
|
6d8948cdf1 | ||
|
|
27ca67b6f9 | ||
|
|
7e27bf0785 | ||
|
|
a4e8cbe9e1 | ||
|
|
3d58a15b10 | ||
|
|
5c80aec50b | ||
|
|
b52916e62c | ||
|
|
91ea0c9c8e | ||
|
|
2a80a10eec | ||
|
|
7b8fb02398 | ||
|
|
74aa319f41 | ||
|
|
0de4856247 | ||
|
|
da61b1a3e7 | ||
|
|
0632213c8b | ||
|
|
daa8de203b | ||
|
|
537558e3d5 | ||
|
|
8d23b262c7 | ||
|
|
04a178eda6 | ||
|
|
977f353f9c | ||
|
|
ae6ff97b62 | ||
|
|
fa45891af4 | ||
|
|
159b36607a | ||
|
|
a731687607 | ||
|
|
a77a74ba61 | ||
|
|
fc35381e1b | ||
|
|
6f837830ab | ||
|
|
8197361fb0 | ||
|
|
c7d12fbdf8 | ||
|
|
0cb3fd3134 | ||
|
|
77a791e5da | ||
|
|
42d94b43b1 | ||
|
|
cb6bf00236 | ||
|
|
4f6bd11a70 | ||
|
|
0f14326449 | ||
|
|
b421b4476b | ||
|
|
18554ec439 | ||
|
|
806e24b9f1 | ||
|
|
fa1d69f1f9 | ||
|
|
33aebc42b3 | ||
|
|
ffcb94674e | ||
|
|
c1323a6ba1 | ||
|
|
ea70da8fbf | ||
|
|
1bdb17f073 | ||
|
|
506031b5cb | ||
|
|
4a981900aa | ||
|
|
6fa052bfcf | ||
|
|
19ac32d328 | ||
|
|
44eab78935 | ||
|
|
a2413f3635 | ||
|
|
13b1d8fc34 | ||
|
|
8b90449970 | ||
|
|
038e76ed94 | ||
|
|
390c80c46c | ||
|
|
dffae008cd | ||
|
|
bcf9af71e2 | ||
|
|
5787d32c1a | ||
|
|
39b5032303 | ||
|
|
5f871b1945 | ||
|
|
346f1c991a | ||
|
|
b14a56bb6c | ||
|
|
f75d0acb1c | ||
|
|
b2d1fd916d | ||
|
|
b7f1e30708 | ||
|
|
88863d8d01 | ||
|
|
c275c54fca | ||
|
|
b97ace2c6e | ||
|
|
c78c4689f1 | ||
|
|
f39ccf7629 | ||
|
|
885dfb5bea | ||
|
|
8037273672 | ||
|
|
2e9c0656de | ||
|
|
e85d294d12 | ||
|
|
24bf6cffc3 | ||
|
|
16762d1a46 | ||
|
|
6a54005472 | ||
|
|
812d910212 | ||
|
|
b7c8fba464 | ||
|
|
89424f6466 | ||
|
|
0411505341 | ||
|
|
6938083463 | ||
|
|
054b286388 | ||
|
|
add1e02d2f | ||
|
|
fd44744029 | ||
|
|
6844e1b435 | ||
|
|
8140380673 | ||
|
|
66ad9e8856 | ||
|
|
da467ec8ee | ||
|
|
f0923c51e6 | ||
|
|
d1953e792a | ||
|
|
b3294eed68 | ||
|
|
37946c5aba | ||
|
|
ce80f65e9f | ||
|
|
4dce42b85f | ||
|
|
88702085bb | ||
|
|
2719522e07 | ||
|
|
dd573945ed | ||
|
|
9cffb19332 | ||
|
|
6631bb593c | ||
|
|
f5959925aa | ||
|
|
5b20cb316b | ||
|
|
8cb7ff8ed4 | ||
|
|
4bcf1f2d9e | ||
|
|
452b077822 | ||
|
|
f37a32ceab | ||
|
|
7182514a64 | ||
|
|
da60131051 | ||
|
|
aa9804d2df | ||
|
|
10c1b9bc29 | ||
|
|
698422a41e | ||
|
|
ac16f40303 | ||
|
|
e41bdecd5b | ||
|
|
c9a0a8d2b5 | ||
|
|
c308940c4b | ||
|
|
99770ccd2f | ||
|
|
2d4bb90acc | ||
|
|
e78d80b99d | ||
|
|
9f5cd6dc88 | ||
|
|
85c0b670da | ||
|
|
813ec7d294 | ||
|
|
d66eb239fa | ||
|
|
9fcc46b5d5 | ||
|
|
3ebf8a2061 | ||
|
|
de98085e84 | ||
|
|
2557b55817 | ||
|
|
128706e8a1 | ||
|
|
d854d8ae0b | ||
|
|
637191836a | ||
|
|
69ab9d96f7 | ||
|
|
073febe24a | ||
|
|
f01a4fcfac | ||
|
|
4f81a4e9b4 | ||
|
|
1a06033964 | ||
|
|
c5dad11e5e | ||
|
|
5ed89754b3 | ||
|
|
ee88be613c | ||
|
|
ebafb4c05e | ||
|
|
70466d0c94 | ||
|
|
340003c568 | ||
|
|
db5343fba3 | ||
|
|
8434842c65 | ||
|
|
2482881117 | ||
|
|
4afe2160e1 | ||
|
|
6225390b7f | ||
|
|
d5a9c98ff9 | ||
|
|
bed1b85319 | ||
|
|
8a728ad28a | ||
|
|
c42f17c96e | ||
|
|
054352356b | ||
|
|
2646f642b5 | ||
|
|
436bdde60a | ||
|
|
07210a23b7 | ||
|
|
da36ef4002 | ||
|
|
e3b06b110f | ||
|
|
792dce6843 | ||
|
|
0ec048ceba | ||
|
|
07631e9b02 | ||
|
|
4834363fb5 | ||
|
|
734dbfb761 | ||
|
|
37044dae01 | ||
|
|
e9ee31b22a | ||
|
|
cf0e3ce989 | ||
|
|
27664164fa | ||
|
|
b7360c426b | ||
|
|
0e176d5608 | ||
|
|
a947a81772 | ||
|
|
bbb4185fac | ||
|
|
e34f0587fe | ||
|
|
0e02dd660c | ||
|
|
2376e511ac | ||
|
|
56bf447cdb | ||
|
|
5ebb1718d6 | ||
|
|
9143b92932 | ||
|
|
fbff4a8cb1 | ||
|
|
b02371e4c3 | ||
|
|
e32a2bbe81 | ||
|
|
10ebd61519 | ||
|
|
7ac90891ca | ||
|
|
711c18a7f1 | ||
|
|
b3cb08316a | ||
|
|
42b8b9ce4a | ||
|
|
fc4834ebd6 | ||
|
|
24ab79a09a | ||
|
|
786a8832d3 | ||
|
|
4d25b6a43c | ||
|
|
29bb54d2cc | ||
|
|
cd075a3559 | ||
|
|
0455a83ef1 | ||
|
|
6c5791b1fe | ||
|
|
705b6cc03d | ||
|
|
b190e54285 | ||
|
|
478db15211 | ||
|
|
1542f3811d | ||
|
|
85dd8029af | ||
|
|
11beb6676e | ||
|
|
99d7fe5ca2 | ||
|
|
248c1ce189 | ||
|
|
bf972681d5 | ||
|
|
996632ac73 | ||
|
|
b28a1986c9 | ||
|
|
fb96e93184 | ||
|
|
ac4db91df4 | ||
|
|
408845878b | ||
|
|
7933d49bb2 | ||
|
|
6945032077 | ||
|
|
0af137ba8c | ||
|
|
de3d376063 | ||
|
|
36da872932 | ||
|
|
ff2ed50dea | ||
|
|
6bb2685e03 | ||
|
|
5eb0424ee7 | ||
|
|
80536ef4fb | ||
|
|
f3bebc6fa2 | ||
|
|
d0bb3cc75c | ||
|
|
98213cff67 | ||
|
|
c074de5876 | ||
|
|
906bdfa15e | ||
|
|
e844164cf6 | ||
|
|
1b47bfa2f1 | ||
|
|
33997c9a82 | ||
|
|
4713a44573 | ||
|
|
be7b0945e9 | ||
|
|
0f1bfc5a17 | ||
|
|
3bc9f1382c | ||
|
|
a08e93d975 | ||
|
|
91a120599f | ||
|
|
5d399b2497 | ||
|
|
0cab18b9b5 | ||
|
|
903da8732d | ||
|
|
6857ae5182 | ||
|
|
5bcbffcdf0 | ||
|
|
953083a0bf | ||
|
|
1810bbe2d5 | ||
|
|
f716ab0c1b | ||
|
|
e04e5596a3 | ||
|
|
f239d498ad | ||
|
|
7b768b5b5b | ||
|
|
60813274dc | ||
|
|
ad5bc4fc0d | ||
|
|
c1c6949175 | ||
|
|
26840700ee | ||
|
|
ec610de266 | ||
|
|
f4129ff5c2 | ||
|
|
b8cc6d9809 | ||
|
|
9384d1d96f | ||
|
|
854b6b76a0 | ||
|
|
bc836e973c | ||
|
|
bf9855abd1 | ||
|
|
4006438d93 | ||
|
|
cdfb86e021 | ||
|
|
81e0232712 | ||
|
|
e67b68aa20 | ||
|
|
a075de4711 | ||
|
|
6d782352f7 | ||
|
|
587993c957 | ||
|
|
aaca3e65ce | ||
|
|
ce53acdacf | ||
|
|
a449bac130 | ||
|
|
060bd32417 | ||
|
|
08a1595d3e | ||
|
|
b88c56b67a | ||
|
|
7deb395fd9 | ||
|
|
2ef104224b | ||
|
|
d84b2f3870 | ||
|
|
7e5467935b | ||
|
|
f7c1714cb8 | ||
|
|
12724cea56 | ||
|
|
2cea66cba5 | ||
|
|
ac01469eac | ||
|
|
cb4cc4e7d9 | ||
|
|
c648493a9e | ||
|
|
a36da6ebde | ||
|
|
4fd2fc7e00 | ||
|
|
39646732b6 | ||
|
|
8791507aca | ||
|
|
c173b2a230 | ||
|
|
05d2df623e | ||
|
|
e75a1fcd12 | ||
|
|
529165f2b5 | ||
|
|
9a5743a27e | ||
|
|
271ef9bf00 | ||
|
|
3a97f7645e | ||
|
|
a84bf9efef | ||
|
|
daa5129f65 | ||
|
|
ac375abf5e | ||
|
|
bae5a7c838 | ||
|
|
1bcb7210c6 | ||
|
|
c15dcf6b98 | ||
|
|
9ead7a413e | ||
|
|
07817b04fb | ||
|
|
e65d67f12e | ||
|
|
e39f013808 | ||
|
|
c34affc215 | ||
|
|
07b8cc04be | ||
|
|
78a396ce4b | ||
|
|
43997f2215 | ||
|
|
e9bac0a737 | ||
|
|
e7c69d2a6b | ||
|
|
8c3400ee41 | ||
|
|
3d79c939e9 | ||
|
|
27b8e6bd21 | ||
|
|
24ecaab570 | ||
|
|
74e39c437d | ||
|
|
c1c7788cd3 | ||
|
|
a2db465825 | ||
|
|
9605264b9a | ||
|
|
24e6808984 | ||
|
|
0d9c7a4cc2 | ||
|
|
3c6ecf1563 | ||
|
|
5c634d6ff9 | ||
|
|
6b51155bac | ||
|
|
1873ad8355 | ||
|
|
8087396188 | ||
|
|
5128db9f6c | ||
|
|
ef832461c0 | ||
|
|
85493cdfd8 | ||
|
|
e557f1361d | ||
|
|
9598ac5a50 | ||
|
|
5ee7add355 | ||
|
|
81c1e6e887 | ||
|
|
b7004b3866 | ||
|
|
5341326811 | ||
|
|
776c08e605 | ||
|
|
b7c5d18df1 | ||
|
|
39dc6c576a | ||
|
|
d9fc262003 | ||
|
|
9b7f2b3a79 | ||
|
|
1ad4ac2d63 | ||
|
|
9ca7949bf0 | ||
|
|
d2174364b2 | ||
|
|
91238c3a9c | ||
|
|
d32c8c999b | ||
|
|
6580b78485 | ||
|
|
0eec8dea05 | ||
|
|
8fa356242e | ||
|
|
a13611f601 | ||
|
|
e8307cdcd9 | ||
|
|
ca425cf949 | ||
|
|
9f57d393bf | ||
|
|
84c59e2c8b | ||
|
|
212f60db60 | ||
|
|
fbbdb056d7 | ||
|
|
e3c0fe5e78 | ||
|
|
a3afb1a2b8 | ||
|
|
ac94b6d917 | ||
|
|
8db9580ff5 | ||
|
|
489c00ebb0 | ||
|
|
d58fe84439 | ||
|
|
ffe3843747 | ||
|
|
b370af3c19 | ||
|
|
cdd3c81bdc | ||
|
|
eb72839e2b | ||
|
|
6457cedd9b | ||
|
|
be72ea0c98 | ||
|
|
bf5691bdb6 | ||
|
|
a6a8734599 | ||
|
|
d0aecc76ed | ||
|
|
3b80b8d7f1 | ||
|
|
c11bb3be59 | ||
|
|
7ee4c14fae | ||
|
|
17a0209c8c | ||
|
|
fc10c05731 | ||
|
|
9ef0d0edfb | ||
|
|
473f4a4295 | ||
|
|
94ba1f0b09 | ||
|
|
8903fa268a | ||
|
|
6ad980d471 | ||
|
|
9a6e9c4660 | ||
|
|
f7c842774b | ||
|
|
fde952fcd9 | ||
|
|
a71c2bd8ec | ||
|
|
7c4f887ef4 | ||
|
|
b32b0e970c | ||
|
|
836f789fc9 | ||
|
|
6b71e2f22a | ||
|
|
1fa83d3f8f | ||
|
|
247f8f132b | ||
|
|
3fcdf38d4a | ||
|
|
0626d1c466 | ||
|
|
f0da7c6300 | ||
|
|
2554478a38 | ||
|
|
d7f24759d8 | ||
|
|
df3bd6fbec | ||
|
|
11ab81a484 | ||
|
|
91e7a092c4 | ||
|
|
bca5130020 | ||
|
|
153029abdd | ||
|
|
03ed3f13f4 | ||
|
|
8c7fd78bd4 | ||
|
|
a813535e3f | ||
|
|
7cae5f8f10 | ||
|
|
070c19d784 | ||
|
|
0833cd8a9b | ||
|
|
64f3a2db58 | ||
|
|
ad8a425d30 | ||
|
|
40d5c8d79f | ||
|
|
c6e471323f | ||
|
|
60a0c8726e | ||
|
|
5772ff78e6 | ||
|
|
222ed29b6c | ||
|
|
23870523fb | ||
|
|
b1a1d36b66 | ||
|
|
2946c51774 | ||
|
|
8b72214780 | ||
|
|
0f701ad2d3 | ||
|
|
d686c48a0b | ||
|
|
80555f9c96 | ||
|
|
ec1155b1ba | ||
|
|
7377386ee2 | ||
|
|
f4bb1101bf | ||
|
|
e44188b830 | ||
|
|
c5d81afdf6 | ||
|
|
f6d55fec35 | ||
|
|
2ef8219f15 | ||
|
|
bf9197b3e4 | ||
|
|
27b1a31436 | ||
|
|
9b51c8cab4 | ||
|
|
114a452609 | ||
|
|
ef85156bae | ||
|
|
e55d17fd08 | ||
|
|
8b83205b0a | ||
|
|
2251350a4e | ||
|
|
f106a31990 | ||
|
|
c9f222583a | ||
|
|
13fc51a8a5 | ||
|
|
21309cddf0 | ||
|
|
86164ba518 | ||
|
|
8173003144 | ||
|
|
f9c8b00587 | ||
|
|
b77da0f143 | ||
|
|
ed955150df | ||
|
|
f282197611 | ||
|
|
93ce932d28 | ||
|
|
29505fa4a3 | ||
|
|
991442d5c0 | ||
|
|
e7b5991dbf | ||
|
|
fb72317c6f | ||
|
|
7992568c0f | ||
|
|
38666b7c99 | ||
|
|
1f3f143ffb | ||
|
|
156ece4bb5 | ||
|
|
770d1ae689 | ||
|
|
d7287c48cf | ||
|
|
b4c329f2f9 | ||
|
|
cb2219e2cd | ||
|
|
65f0bfa8a4 | ||
|
|
7090e0a47b | ||
|
|
fbb4d61194 | ||
|
|
634b0b50ff | ||
|
|
7ff66e9277 | ||
|
|
b93cc3ab20 | ||
|
|
55e7052189 | ||
|
|
5a9808de59 | ||
|
|
a30b34df70 | ||
|
|
482b19dd5a | ||
|
|
7895ed89f1 | ||
|
|
8449853076 | ||
|
|
affec8d3c1 | ||
|
|
362b69d921 | ||
|
|
9d463c7b4a | ||
|
|
cd7da64794 | ||
|
|
5c95f2971f | ||
|
|
ab3f3f0633 | ||
|
|
ba61ac46d1 | ||
|
|
48205d8a6c | ||
|
|
b4cbb1fd14 | ||
|
|
c8db3ec762 | ||
|
|
f6cf157930 | ||
|
|
4a84a9ed8e | ||
|
|
dbb41ba249 | ||
|
|
d3adec5a23 | ||
|
|
1fd030f909 | ||
|
|
b6dfeb475d | ||
|
|
39050c6de6 | ||
|
|
c36926c915 | ||
|
|
3bf3241bd7 | ||
|
|
df863e879f | ||
|
|
b13af00061 | ||
|
|
1c2215a8a2 | ||
|
|
555ae35bb9 | ||
|
|
8337a1698e | ||
|
|
f0bfa96937 | ||
|
|
7b143dd38f | ||
|
|
5201c5933c | ||
|
|
4c1d501856 | ||
|
|
e001533f33 | ||
|
|
c854dd9a45 | ||
|
|
4215b39539 | ||
|
|
8e882aafa1 | ||
|
|
25edbf06c7 | ||
|
|
4f05365da3 | ||
|
|
bf7de99524 | ||
|
|
08c27b6c58 | ||
|
|
60661757c6 | ||
|
|
5ba64483fb | ||
|
|
a30c9391eb | ||
|
|
7023e726bd | ||
|
|
c616ea81c6 | ||
|
|
65f8b587af | ||
|
|
14f6b9c759 | ||
|
|
c83e0f8cff | ||
|
|
2bf86423c9 | ||
|
|
8dc3035b66 | ||
|
|
0aff0330e7 | ||
|
|
41852f2467 | ||
|
|
442ec76828 | ||
|
|
d8fc14e71b | ||
|
|
2630d32764 | ||
|
|
74d7eff577 | ||
|
|
355c9e2a3d | ||
|
|
501bf06ada | ||
|
|
4574bc0b2f | ||
|
|
c6c91b84fe | ||
|
|
066b33e3e8 | ||
|
|
15002c45d6 | ||
|
|
b41f3b9370 | ||
|
|
02e2700e96 | ||
|
|
6441dfd219 | ||
|
|
1e229c12cc | ||
|
|
4219f2db5b | ||
|
|
36ce636093 | ||
|
|
47f09f81ff | ||
|
|
bfa6df904d | ||
|
|
99d4f55c50 | ||
|
|
7728b4b1ab | ||
|
|
6a475434ad | ||
|
|
876a0bd108 | ||
|
|
ba13bfd9ad | ||
|
|
44bab8c0c7 | ||
|
|
2d229b82c3 | ||
|
|
c6cce7aa9a | ||
|
|
5cbf09f24e | ||
|
|
3ca7d3d615 | ||
|
|
25840be694 | ||
|
|
34ab608425 | ||
|
|
b498160b3a | ||
|
|
23aae3b5b9 | ||
|
|
97fdd096a8 | ||
|
|
0d21a02da9 | ||
|
|
ab94250b05 | ||
|
|
42ca6f79dc | ||
|
|
646795b753 | ||
|
|
de649915e2 | ||
|
|
0093f5a0de | ||
|
|
686b50eeda | ||
|
|
0c93b85024 | ||
|
|
49d4e88022 | ||
|
|
3a8616e225 | ||
|
|
d4178c85a9 | ||
|
|
de46790bdf | ||
|
|
c27070ae28 | ||
|
|
bb4c9477da | ||
|
|
95e78e4f93 | ||
|
|
d3d7b5a5c7 | ||
|
|
45a3d74284 | ||
|
|
cd972b5c61 | ||
|
|
341be8bdc1 | ||
|
|
101c24edc5 | ||
|
|
be34915cdf | ||
|
|
70aed1d5db | ||
|
|
d8c1144881 | ||
|
|
68ec7f504a | ||
|
|
3ab2b16042 | ||
|
|
d7fc6f9f49 | ||
|
|
26a72244c0 | ||
|
|
abc21badb1 | ||
|
|
aa1caacfd6 | ||
|
|
3663eb63e7 | ||
|
|
e885700680 | ||
|
|
d6170d602a | ||
|
|
4dbb287e11 | ||
|
|
e537e0f115 | ||
|
|
3613d7a37b | ||
|
|
b5e98f505f | ||
|
|
3dc9ca6822 | ||
|
|
e13ddeaaad | ||
|
|
56ce7f9696 | ||
|
|
4ebddf78ed | ||
|
|
2682165da8 | ||
|
|
373e7dc8ad | ||
|
|
0551f8bff1 | ||
|
|
b4454cc812 | ||
|
|
40fb1c8868 | ||
|
|
f97cb4a1bb | ||
|
|
56d0786702 | ||
|
|
d880ecd709 | ||
|
|
1bee9e19e6 | ||
|
|
c5879ae5a7 | ||
|
|
64f458e15a | ||
|
|
2fa5d09fc9 | ||
|
|
d6fc60e02b | ||
|
|
bb9bfd6396 | ||
|
|
0fbe139e8d | ||
|
|
571591f021 | ||
|
|
8a1d2383b8 | ||
|
|
00c5edcea7 | ||
|
|
39d62099df | ||
|
|
c5d6f6f5b9 | ||
|
|
13f3cf1e90 | ||
|
|
93f6bf8ba3 | ||
|
|
1cdbed51cd | ||
|
|
50ae04bb4e | ||
|
|
a55d503faa | ||
|
|
7fc4f83eb5 | ||
|
|
bc831b4d30 | ||
|
|
c6190146aa | ||
|
|
3f01152a4a | ||
|
|
ad5c652a8f | ||
|
|
9609db941b | ||
|
|
bbb8447f5c | ||
|
|
22ca06af3e | ||
|
|
af11758190 | ||
|
|
32fcc25ea4 | ||
|
|
b3fcf8dd5e | ||
|
|
b7d2a97f05 | ||
|
|
ad13875137 | ||
|
|
e14a131480 | ||
|
|
64ba0db228 | ||
|
|
c99d8e7e75 | ||
|
|
305d1cea94 | ||
|
|
f314b3982e | ||
|
|
5ea2615b93 | ||
|
|
17123fec35 | ||
|
|
73d05e7cbf | ||
|
|
3380ea3609 | ||
|
|
101527d3e1 | ||
|
|
df9ab3250c | ||
|
|
d533df52de | ||
|
|
d2cf16d046 | ||
|
|
40a65b5e13 | ||
|
|
fa33cb680e | ||
|
|
2757eb91ce | ||
|
|
2842429ced | ||
|
|
fb2a26c5b7 | ||
|
|
cab1dc8838 | ||
|
|
0ec76dcde3 | ||
|
|
c41046953e | ||
|
|
30f740a430 | ||
|
|
1e8c0ce99b | ||
|
|
aa3a3d9181 | ||
|
|
6d0f528201 | ||
|
|
131cd5915c | ||
|
|
f5512fa162 | ||
|
|
484bb758ae | ||
|
|
89461893a4 | ||
|
|
54e865feb2 | ||
|
|
015fa3dc9f | ||
|
|
cf015be49f | ||
|
|
b6c8993f7e | ||
|
|
1ef37d91e8 | ||
|
|
7fc81cf363 | ||
|
|
123bf9de34 | ||
|
|
d3f6b75d34 | ||
|
|
a5fe5f53e2 | ||
|
|
e91029f66e | ||
|
|
2a7ce54c28 | ||
|
|
f3b2d4dc57 | ||
|
|
95b9871f7f | ||
|
|
533af83749 | ||
|
|
e4330fee92 | ||
|
|
5fec57e8e3 | ||
|
|
95a06d572b | ||
|
|
cc8c125934 | ||
|
|
91c605ee4b | ||
|
|
f44fe4def1 | ||
|
|
7c0a1ea089 | ||
|
|
ce5c4d1111 | ||
|
|
98952972a0 | ||
|
|
f728b6ab1b | ||
|
|
e799b48877 | ||
|
|
fc8ecb7470 | ||
|
|
ac18234e29 | ||
|
|
59b17aa47e | ||
|
|
9155bfb886 | ||
|
|
cbe683d25e | ||
|
|
64f057a415 | ||
|
|
3da308346e | ||
|
|
313dd681de | ||
|
|
aaf7e1e3e5 | ||
|
|
9f831fd8b5 | ||
|
|
450397481e | ||
|
|
6fb8978f48 | ||
|
|
4a7bf4b31e | ||
|
|
38e94210e4 | ||
|
|
d338809750 | ||
|
|
7fd7e17d1d | ||
|
|
a5a5bd80c4 | ||
|
|
d264ca1ed4 | ||
|
|
85dd19509c | ||
|
|
faf1945933 | ||
|
|
3c6d6ff702 | ||
|
|
c6ecc89ad3 | ||
|
|
abed79441d | ||
|
|
906199a517 | ||
|
|
6f34c21d94 | ||
|
|
9ba1a68b51 | ||
|
|
073c72fd63 | ||
|
|
3ac4af1558 | ||
|
|
5057f6848f | ||
|
|
b3955731c2 | ||
|
|
af83cf552e | ||
|
|
a0426044e8 | ||
|
|
432635d567 | ||
|
|
02b78320ec | ||
|
|
f1461f905d | ||
|
|
e1cdb3ab65 | ||
|
|
6218424be3 | ||
|
|
5a9b7e296f | ||
|
|
f4cc9c7734 | ||
|
|
e0c7998448 | ||
|
|
752d65557f | ||
|
|
a952674df7 | ||
|
|
25f5f6e1f7 | ||
|
|
0f1d6c0984 | ||
|
|
8dd4bb9d61 | ||
|
|
98275ade59 | ||
|
|
95cc9f0e21 | ||
|
|
742ea50c2c | ||
|
|
54b1174e1b | ||
|
|
e07de72fa4 | ||
|
|
13b4af3734 | ||
|
|
29566a6c93 | ||
|
|
7669f7d9a0 | ||
|
|
5913d5b585 | ||
|
|
d9a332de44 | ||
|
|
e85533686e | ||
|
|
0100140dc0 | ||
|
|
8def9e8931 | ||
|
|
6fc2f26983 | ||
|
|
ed1ed6cbe9 | ||
|
|
10d19a5392 | ||
|
|
7bbd90ab91 | ||
|
|
9565d48b04 | ||
|
|
284d366b44 | ||
|
|
a9893379f4 | ||
|
|
50677ad81d | ||
|
|
71d7fcbe65 | ||
|
|
8342acbd49 | ||
|
|
d5296763ad | ||
|
|
73efa4fe91 | ||
|
|
82f573e1a1 | ||
|
|
4ef0ac3fee | ||
|
|
bc246f39d2 | ||
|
|
f9af23dbca | ||
|
|
68f8ef0b24 | ||
|
|
16f6acf8fc | ||
|
|
3faa5b2f52 | ||
|
|
04a7c068f4 | ||
|
|
92dee27634 | ||
|
|
7dda25f96b | ||
|
|
40f700910a | ||
|
|
aa90b34511 | ||
|
|
45cf082bb9 | ||
|
|
0ab78983d4 | ||
|
|
e137210cbc | ||
|
|
3093755c9e | ||
|
|
94c4950d23 | ||
|
|
fa0a624b7c | ||
|
|
f3fabe1708 | ||
|
|
52ec890e2c | ||
|
|
2a10471e0b | ||
|
|
94c1974d2f | ||
|
|
f0a8014efb | ||
|
|
96cac6ca68 | ||
|
|
0da50bc693 | ||
|
|
1169ac44b4 | ||
|
|
38367279ff | ||
|
|
ef00cfbddd | ||
|
|
e514e4b64e | ||
|
|
f1af07e921 | ||
|
|
44cf022e70 | ||
|
|
7e68de5a17 | ||
|
|
51f73d77bf | ||
|
|
fa4ccf07b8 | ||
|
|
49da7aafd0 | ||
|
|
58ca71baaa | ||
|
|
2c550a0874 | ||
|
|
dcb671acd8 | ||
|
|
56600d3f27 | ||
|
|
6913f7bdf5 | ||
|
|
bcc2d286ed | ||
|
|
aed6c2123f | ||
|
|
68626fecd7 | ||
|
|
441d7376cb | ||
|
|
c9ac38de01 | ||
|
|
b9c063c41e | ||
|
|
55804be70e | ||
|
|
2b749af917 | ||
|
|
9378dfdd04 | ||
|
|
46768c77b7 | ||
|
|
ff9358b986 | ||
|
|
f3090870be | ||
|
|
666fbe8ce7 | ||
|
|
67b29851ea | ||
|
|
1a2cd201a7 | ||
|
|
5041bf67a5 | ||
|
|
1a2f3bf80e | ||
|
|
d7a231eb18 | ||
|
|
21c9c190aa | ||
|
|
a781c2d665 | ||
|
|
09c3ce3164 | ||
|
|
a7888a63fa | ||
|
|
79dd56d017 | ||
|
|
882d04f50c | ||
|
|
a7fd414ce6 | ||
|
|
eb30451cfa | ||
|
|
a48f2b1f17 | ||
|
|
d3665d64a6 | ||
|
|
a4bbc9c3c6 | ||
|
|
b0ffda42bc | ||
|
|
b333045d41 | ||
|
|
ef5d0a81eb | ||
|
|
48819c10a9 | ||
|
|
9ae23e4395 | ||
|
|
4d36efebb7 | ||
|
|
e842a37654 | ||
|
|
c821626dc1 | ||
|
|
16aba9ff96 | ||
|
|
a11bfdb13b | ||
|
|
c0437e55eb | ||
|
|
279bcbfeab | ||
|
|
6555263496 | ||
|
|
8db4e17a8a | ||
|
|
4a5bd3135f | ||
|
|
32eb98361a | ||
|
|
2ba5073d55 | ||
|
|
9ea5e2cd90 | ||
|
|
284a1f0b57 | ||
|
|
7b97f3d535 | ||
|
|
fb431fcc7b | ||
|
|
b16100e627 | ||
|
|
fde6b5df9b | ||
|
|
dc1ac106c0 | ||
|
|
0d4a26c05e | ||
|
|
ffe7a9294b | ||
|
|
15004b6ba2 | ||
|
|
dfd8d1b0c9 | ||
|
|
c2fef3fa25 | ||
|
|
d33c892303 | ||
|
|
e4ce97cf5d | ||
|
|
f331916bd5 | ||
|
|
5d3194dd41 | ||
|
|
f2574b516e | ||
|
|
9ee3e973c1 | ||
|
|
eb9e797017 | ||
|
|
d0c305b3eb | ||
|
|
db2a7208da | ||
|
|
e8958f5e53 | ||
|
|
8acee1251f | ||
|
|
acf117e43b | ||
|
|
ed1b7f477b | ||
|
|
63cf1f5fa1 | ||
|
|
fa9b738cba | ||
|
|
3efbefe4c5 | ||
|
|
dcb797db38 | ||
|
|
8f9f9e9e82 | ||
|
|
642754a46b | ||
|
|
a5578335d3 | ||
|
|
c4309aa14c | ||
|
|
af3eb10034 | ||
|
|
88b7804123 | ||
|
|
4a383521d7 | ||
|
|
279b65cfa0 | ||
|
|
6ce644ea18 | ||
|
|
9ef5e8d037 | ||
|
|
a9e14a93dd | ||
|
|
8e2c0d8653 | ||
|
|
39a19fd9e6 | ||
|
|
68e1a0489f | ||
|
|
ece64c3f4a | ||
|
|
37f67469a6 | ||
|
|
a1b546152b | ||
|
|
3ae1f6c556 | ||
|
|
14757e2a35 | ||
|
|
d20a762dd8 | ||
|
|
5ff79f5ee1 | ||
|
|
afe28733db | ||
|
|
5e5fe6013d | ||
|
|
3f0196c8f8 | ||
|
|
4e38cf9d40 | ||
|
|
6026f6aebd | ||
|
|
856bcf1647 | ||
|
|
8e7196181c | ||
|
|
beb6697507 | ||
|
|
fd482d32a7 | ||
|
|
10169b03ce | ||
|
|
5f4a7cd7c9 | ||
|
|
59b52b8a47 | ||
|
|
02bffab38f | ||
|
|
256f08396f | ||
|
|
988a280111 | ||
|
|
7a44a457d5 | ||
|
|
2fa153e569 | ||
|
|
b589c78cfc | ||
|
|
293acbcc03 | ||
|
|
be0a841926 | ||
|
|
763cbfa656 | ||
|
|
d149b02c06 | ||
|
|
b83773dfa6 | ||
|
|
c992701387 | ||
|
|
bf43944c27 | ||
|
|
6dbed875e1 | ||
|
|
5166387f34 | ||
|
|
b44121597f | ||
|
|
f086b8abe9 | ||
|
|
795da9557b | ||
|
|
76249cb8f7 | ||
|
|
f4c4162e4b | ||
|
|
902cc9009e | ||
|
|
d93c2ee267 | ||
|
|
e5b864f07e | ||
|
|
8df4cd3dd6 | ||
|
|
2fdb6f15cb | ||
|
|
c7e493d7f5 | ||
|
|
043537a7b4 | ||
|
|
e7643f3894 | ||
|
|
bb2c8ae8e5 | ||
|
|
1c8860c596 | ||
|
|
66e65fcd14 | ||
|
|
7cef8f24db | ||
|
|
a82f3a7b07 | ||
|
|
4f41068c99 | ||
|
|
50dfb95c48 | ||
|
|
0b29ac00a7 | ||
|
|
759519d374 | ||
|
|
3d713b13da | ||
|
|
fcbe52539a | ||
|
|
bcd64286cd | ||
|
|
b89147120c | ||
|
|
c9ffd3cd11 | ||
|
|
cd62f31c17 | ||
|
|
98d63b880b | ||
|
|
04e11b0fea | ||
|
|
a873b28d9b | ||
|
|
b1b2ff6b8c | ||
|
|
cb96b5fa8f | ||
|
|
eb960209bf | ||
|
|
51a0e46f8c | ||
|
|
1251205fdd | ||
|
|
9a05629144 | ||
|
|
e0c71006d5 | ||
|
|
3d716a516a | ||
|
|
096648b2d7 | ||
|
|
02e57707de | ||
|
|
20468e612d | ||
|
|
af3aa497d1 | ||
|
|
bb53d1448b | ||
|
|
eccdd73908 | ||
|
|
1d4e1a8be2 | ||
|
|
8dc0f299a9 | ||
|
|
db1ffd5091 | ||
|
|
64a27e5708 | ||
|
|
214a356135 | ||
|
|
84f7953f21 | ||
|
|
a9a947203d | ||
|
|
8f250e755e | ||
|
|
30a7fa7ead | ||
|
|
4324d655d2 | ||
|
|
da1be9226a | ||
|
|
5597bf5d1e | ||
|
|
50029fbb24 | ||
|
|
f5c2dc747d | ||
|
|
dd55e336e4 | ||
|
|
a001a45cc4 | ||
|
|
49320ff623 | ||
|
|
8e898c50b4 | ||
|
|
5fa93e2a2a | ||
|
|
178cca1611 | ||
|
|
9e543e2c5a | ||
|
|
6a16e5534d | ||
|
|
2d1cad870b | ||
|
|
3fdf255ca5 | ||
|
|
ec9225dbf2 | ||
|
|
b469fe92dd | ||
|
|
dbc54b016c | ||
|
|
2cbad36f80 | ||
|
|
8b21f33eb6 | ||
|
|
1e8ff7dbc0 | ||
|
|
0937915839 | ||
|
|
a128546954 | ||
|
|
806953d107 | ||
|
|
afa39b29ed | ||
|
|
4c899555dd | ||
|
|
3e8395b0c6 | ||
|
|
a620f02d57 | ||
|
|
79a078fb70 | ||
|
|
cac2f49b06 | ||
|
|
06993b9d66 | ||
|
|
fce5ca592a | ||
|
|
e7058cf3c8 | ||
|
|
a2eeac786e | ||
|
|
ff7873313b | ||
|
|
f3b06ac0a6 | ||
|
|
00e57fc17d | ||
|
|
c8cc59aaca | ||
|
|
54057922f6 | ||
|
|
c175173821 | ||
|
|
52e9285551 | ||
|
|
823eea1f0a | ||
|
|
ae03a700de | ||
|
|
a089eade6e | ||
|
|
f8fb3d8a70 | ||
|
|
00a75f154e | ||
|
|
8d2313d799 | ||
|
|
0b8384fc3b | ||
|
|
73ab9efdb4 | ||
|
|
705f69510b | ||
|
|
f7e0a33935 | ||
|
|
729015d719 | ||
|
|
d83127a265 | ||
|
|
6e8f7ae698 | ||
|
|
17ee24286c | ||
|
|
133140bf71 | ||
|
|
5fafbf9ee8 | ||
|
|
6085da15a4 | ||
|
|
b93caf1839 | ||
|
|
c9787a521b | ||
|
|
26cabef74c | ||
|
|
8d2474768b | ||
|
|
a87146a401 | ||
|
|
bbf5ee5395 | ||
|
|
103ae607be | ||
|
|
6f63998000 | ||
|
|
a94952babc | ||
|
|
4b8385419e | ||
|
|
878a543818 | ||
|
|
e7337777cd | ||
|
|
fa1ed52c32 | ||
|
|
eda3fccb51 | ||
|
|
ec55fae1ec | ||
|
|
c88a98e355 | ||
|
|
0567e0f251 | ||
|
|
befa7d0e8e | ||
|
|
91913da205 | ||
|
|
cd8677a26d | ||
|
|
adf091e300 | ||
|
|
aa3b831a68 | ||
|
|
ab075c0554 | ||
|
|
4202012bbd | ||
|
|
739854935d | ||
|
|
3d88e734df | ||
|
|
a716a69b8b | ||
|
|
7ac7aacb6c | ||
|
|
7368001e3b | ||
|
|
574e0b4074 | ||
|
|
5d258eb8e1 | ||
|
|
5715fa97f7 | ||
|
|
7fe869b98b | ||
|
|
37aaa30387 | ||
|
|
c3b2d7653f | ||
|
|
cb22161156 | ||
|
|
6ad06c2d75 | ||
|
|
b291dd0ad7 | ||
|
|
3039a32f29 | ||
|
|
a8f5380070 | ||
|
|
e37867b9db | ||
|
|
110d6c81ee | ||
|
|
3da44ce604 | ||
|
|
9770c15188 | ||
|
|
cc568d9569 | ||
|
|
500a4b0b7e | ||
|
|
55cfb7b358 | ||
|
|
77b2f90259 | ||
|
|
e962baaf48 | ||
|
|
2cb5b18975 | ||
|
|
32084eb1e7 | ||
|
|
2815d76b1d | ||
|
|
9c4f146778 | ||
|
|
7b89016359 | ||
|
|
c7a4902af0 | ||
|
|
6f9b686317 | ||
|
|
16550e7a83 | ||
|
|
a9aa47e390 | ||
|
|
0846fb94db | ||
|
|
4c3aafd266 | ||
|
|
1e606f8b85 | ||
|
|
58596377b1 | ||
|
|
94a3234874 | ||
|
|
7e9db5b52d | ||
|
|
dc098d1ec7 | ||
|
|
195a8b4315 | ||
|
|
7df80bc56a | ||
|
|
1c541a4adf | ||
|
|
f29f563e50 | ||
|
|
f4280c0768 | ||
|
|
295ae14658 | ||
|
|
ccc2bcffce | ||
|
|
3a94ef57e3 | ||
|
|
db8d8db280 | ||
|
|
fdcef95d07 | ||
|
|
7b1d9a777d | ||
|
|
3cd8764dbf | ||
|
|
32826440cb | ||
|
|
a65fa8cf10 | ||
|
|
0ae29b1920 | ||
|
|
5748a11788 | ||
|
|
7d3579af4f | ||
|
|
e0dc3bd1f4 | ||
|
|
6200c56144 | ||
|
|
0b4a0eeb55 | ||
|
|
467f5bd2eb | ||
|
|
322d90adfa | ||
|
|
11eb7c058f | ||
|
|
361b251952 | ||
|
|
3d503971ae | ||
|
|
9faabba361 | ||
|
|
f33629aba1 | ||
|
|
a47ed71799 | ||
|
|
4a9b9d57e4 | ||
|
|
98c3f0ce5b | ||
|
|
78a3082bcb | ||
|
|
b64af43a7e | ||
|
|
1bcacbfebe | ||
|
|
f32db6c83b | ||
|
|
437b638973 | ||
|
|
670918efd3 | ||
|
|
006a49cfdb | ||
|
|
9794ee259a | ||
|
|
643e0e0c1f | ||
|
|
6afcb364d1 | ||
|
|
6d4a38404c | ||
|
|
b925c2ef20 | ||
|
|
e3f931d4f5 | ||
|
|
01b5d63972 | ||
|
|
97794ce7c5 | ||
|
|
ac4c66a1f7 | ||
|
|
9750e26d4b | ||
|
|
4b14412190 | ||
|
|
207c2e10e3 | ||
|
|
7c73515427 | ||
|
|
aea7108940 | ||
|
|
2bdd97d889 | ||
|
|
93b6dd3374 | ||
|
|
bf3c123658 | ||
|
|
06c0a361fd | ||
|
|
92510845d6 | ||
|
|
8ab57859f6 | ||
|
|
0608b50193 | ||
|
|
e14ff26915 | ||
|
|
8932a16468 | ||
|
|
3804db142f | ||
|
|
7e198bd7a1 | ||
|
|
85301c92ec | ||
|
|
2ca6be77ed | ||
|
|
48558bec0f | ||
|
|
31af8669b5 | ||
|
|
d716ee5d26 | ||
|
|
8e1c07d530 | ||
|
|
0d7b52aadc | ||
|
|
25eb99f014 | ||
|
|
ab8276df2f | ||
|
|
68569ad875 | ||
|
|
c0e77d9eec | ||
|
|
27297c5d24 | ||
|
|
b6a7a02b23 | ||
|
|
534e20a072 | ||
|
|
9478da81a9 | ||
|
|
ee958f20d2 | ||
|
|
00edf44828 | ||
|
|
f3a49533fd | ||
|
|
3b6517090c | ||
|
|
5b2687ae83 | ||
|
|
c3402e8d44 | ||
|
|
a9625dfecd | ||
|
|
d6c8464e97 | ||
|
|
1b557d5f8c | ||
|
|
e1cf944db7 | ||
|
|
cb873efd38 | ||
|
|
ee5acfa35f | ||
|
|
2904c55f84 | ||
|
|
4667e9652f | ||
|
|
905b4fe92e | ||
|
|
85ccc78f8f | ||
|
|
ad3bb89dc9 | ||
|
|
106bded9b6 | ||
|
|
f46c8a03d9 | ||
|
|
5d9693c419 | ||
|
|
126546a938 | ||
|
|
bb3902730b | ||
|
|
1b9e25e81c | ||
|
|
b11439ca87 | ||
|
|
66034ea407 | ||
|
|
6690c665dd | ||
|
|
4b71cd9940 | ||
|
|
8b5ef24681 | ||
|
|
e161890eaa | ||
|
|
348f27237b | ||
|
|
c19164269a | ||
|
|
071491b459 | ||
|
|
e1180a9a14 | ||
|
|
f76a027b32 | ||
|
|
cc1a91e5cd | ||
|
|
fcee4d13da | ||
|
|
fa567ce0e2 | ||
|
|
c10085b65a | ||
|
|
a8465408cf | ||
|
|
d123a50054 | ||
|
|
bd09e4017a | ||
|
|
8b5e29d29e | ||
|
|
6e427b060a | ||
|
|
7d0f70f1c0 | ||
|
|
5f1ca64d65 | ||
|
|
fb58f08e44 | ||
|
|
017d00371d | ||
|
|
abe1d3ad29 | ||
|
|
fc48ba1994 | ||
|
|
279fe5dcb8 | ||
|
|
ccb3f7ef34 | ||
|
|
de12ec6548 | ||
|
|
14bd2480ce | ||
|
|
8152b9ab0d | ||
|
|
e6e4782d51 | ||
|
|
aa5f4fb986 | ||
|
|
aa4fe50eeb | ||
|
|
df072f1c40 | ||
|
|
9b3e202eb8 | ||
|
|
b403189afb | ||
|
|
82d076b87d | ||
|
|
dcdf951ebc | ||
|
|
4c1f8e4005 | ||
|
|
672ecc7f0a | ||
|
|
81f05528d8 | ||
|
|
57214aadfc | ||
|
|
6209a0120b | ||
|
|
98dcb3fbcb | ||
|
|
2211e1c816 | ||
|
|
36bf37da8d | ||
|
|
21be74fb05 | ||
|
|
870ca6cd7f | ||
|
|
4e832a5eb2 | ||
|
|
7b9c23c203 | ||
|
|
fc91807e07 | ||
|
|
8aab359b0b | ||
|
|
363c9ff028 | ||
|
|
2a800a825b | ||
|
|
36c1b1e0dd | ||
|
|
01d7cdc7de | ||
|
|
6f25cb9017 | ||
|
|
c4ff479af4 | ||
|
|
cc8406cd79 | ||
|
|
b94c1915a8 | ||
|
|
99a6685845 | ||
|
|
10bf60126e | ||
|
|
14d8f0730f | ||
|
|
618503ccf2 | ||
|
|
f64a837172 | ||
|
|
41d4de6150 | ||
|
|
bcf3333dd2 | ||
|
|
47fda64c90 | ||
|
|
4fce05b7d1 | ||
|
|
363bcc6060 | ||
|
|
bc734a5d25 | ||
|
|
3f95678098 | ||
|
|
e52c971aed | ||
|
|
1f82067752 | ||
|
|
0ac1a4c088 | ||
|
|
0fda25b482 | ||
|
|
dd70265cb6 | ||
|
|
62e7d48f3c | ||
|
|
4905106953 | ||
|
|
48edab452d | ||
|
|
7a7076b174 | ||
|
|
c8e1c5fbb7 | ||
|
|
50ceaf6097 | ||
|
|
722d415e75 | ||
|
|
0d4221203b | ||
|
|
4a64895e81 | ||
|
|
e271cb4555 | ||
|
|
0bf54e666b | ||
|
|
0913ef2060 | ||
|
|
7cc9fb11b6 | ||
|
|
9c51b1e0ee | ||
|
|
ba1f458907 | ||
|
|
e7848cb965 | ||
|
|
564ada10f5 | ||
|
|
48721ef7a9 | ||
|
|
bae06e2187 | ||
|
|
8b8e6cb422 | ||
|
|
2ecc990aae | ||
|
|
b47a841207 |
62
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
# Trigger the workflow on push to the main branch or on manual dispatch
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 1. Checkout the repository
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 2. Set up QEMU and Docker Buildx (for multi-platform builds, optional)
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 3. Log in to Docker Hub
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: hykilpikonna
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# 3. Log in to GitHub Container Registry (ghcr.io)
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
# Use GITHUB_TOKEN for authentication
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 4. Cache Docker layers to speed up builds (optional but recommended)
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
# 5. Build and push the Docker image
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
hykilpikonna/aquadx:latest
|
||||
ghcr.io/${{ github.repository_owner }}/AquaDX:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
27
.github/workflows/gradle.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Gradle Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
server-id: github
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
mkdir data
|
||||
./gradlew build
|
||||
51
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# Build script credit to https://github.com/OpenIntelWireless/itlwm/blob/master/.github/workflows/main.yml
|
||||
name: Nightly Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths: ['src/**']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: '10'
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
server-id: github
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Build Artifact
|
||||
run: bash ./tools/build.sh
|
||||
|
||||
- name: Delete previous nightly release
|
||||
run: |
|
||||
gh release delete --cleanup-tag --yes --repo $GITHUB_REPOSITORY nightly || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish GitHub Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
bodyFile: "build/release/ReleaseNotes.md"
|
||||
artifacts: "build/libs/aqua-nightly.jar,build/aqua-nightly.zip"
|
||||
tag: "nightly"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: false
|
||||
|
||||
- name: Mark release undraft
|
||||
run: |
|
||||
gh release edit nightly --draft=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
9
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
db/
|
||||
web/
|
||||
bin/
|
||||
|
||||
@@ -75,4 +76,10 @@ gradle-app.setting
|
||||
|
||||
### Gradle Patch ###
|
||||
# Java heap dump
|
||||
*.hprof
|
||||
*.hprof
|
||||
.jpb
|
||||
src/main/resources/meta/*/*.json
|
||||
*.log.*.gz
|
||||
*.salive
|
||||
test-diff
|
||||
htmlReport
|
||||
|
||||
20
AquaNet/.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Matches multiple files: .ts, .json, .svelte .sass
|
||||
[*.{json,svelte,ts,sass}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
# Markdown files (e.g., README.md) often use a line length of 80 characters
|
||||
[*.md]
|
||||
max_line_length = 80
|
||||
trim_trailing_whitespace = false
|
||||
41
AquaNet/.eslintrc.cjs
Normal file
@@ -0,0 +1,41 @@
|
||||
// ..eslintrc.cjs example
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2023: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '..eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
// Custom styling rules
|
||||
'comma-dangle': ['warn', 'only-multiline'],
|
||||
'indent': ['warn', 2],
|
||||
'semi': ['warn', 'never'],
|
||||
'quotes': ['warn', 'single'],
|
||||
'arrow-parens': ['warn', 'as-needed'],
|
||||
'linebreak-style': ['warn', 'unix'],
|
||||
'object-curly-spacing': ['warn', 'always'],
|
||||
'array-bracket-spacing': ["error", "always", {
|
||||
"singleValue": false,
|
||||
"objectsInArrays": false,
|
||||
"arraysInArrays": false
|
||||
}],
|
||||
|
||||
// Disabled rules
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
}
|
||||
33
AquaNet/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Yarn 3 files
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
893
AquaNet/.yarn/releases/yarn-4.1.1.cjs
vendored
Normal file
20
AquaNet/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# AquaNet
|
||||
|
||||
This is the codebase for the new frontend of AquaDX.
|
||||
This project is also heavily WIP, so more details will be added later on.
|
||||
|
||||
## Development
|
||||
|
||||
This project uses Svelte (NOT SvelteKit) + TypeScript + Sass, built using Vite. The preferred editor is VSCode.
|
||||
|
||||
### Running locally
|
||||
|
||||
First, you would need to install Node.js and bun.
|
||||
Then, you would need to start your testing AquaDX server and configure the `aqua_host` in `src/libs/config.ts` to use your URL.
|
||||
Please leave `data_host` unchanged if you're not sure what it is.
|
||||
Finally, run:
|
||||
|
||||
```shell
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
BIN
AquaNet/bun.lockb
Normal file
45
AquaNet/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AquaDX.net</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
|
||||
<link rel="manifest" href="/assets/icons/site.webmanifest">
|
||||
<link rel="mask-icon" href="/assets/icons/safari-pinned-tab.svg" color="#b3c6ff">
|
||||
<link rel="shortcut icon" href="/assets/icons/favicon.ico">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-config" content="/assets/icons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Font CSS -->
|
||||
<link rel="stylesheet" href="/assets/fonts/Quicksand.400.css" />
|
||||
<link rel="stylesheet" href="/assets/fonts/Quicksand.500.css" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<meta name="title" content="AquaDX.net" />
|
||||
<meta name="description" content="A certain magical arcade server 🪄" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://aquadx.net/" />
|
||||
<meta property="og:title" content="AquaDX.net" />
|
||||
<meta property="og:description" content="A certain magical arcade server 🪄" />
|
||||
<meta property="og:image" content="https://aquadx.net/assets/meta/meta.png" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://aquadx.net/" />
|
||||
<meta property="twitter:title" content="AquaDX.net" />
|
||||
<meta property="twitter:description" content="A certain magical arcade server 🪄" />
|
||||
<meta property="twitter:image" content="https://aquadx.net/assets/meta/meta.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
AquaNet/nginx.conf.example
Normal file
@@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name aqua.example.com;
|
||||
ssl_certificate /etc/nginx/certs/aqua.example.com.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/aqua.example.com.key;
|
||||
|
||||
# Absolute path to store the compiled AquaNet,
|
||||
# there should be index.html in the directory.
|
||||
location / {
|
||||
root /var/www/html/AquaNet/;
|
||||
try_files $uri $uri/ @router;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
# Route for redirection to index.html
|
||||
location @router{
|
||||
rewrite ^.*$ /index.html last;
|
||||
}
|
||||
|
||||
# If you have modified DATA_HOST,
|
||||
# you will need to prepare your own resources.
|
||||
# location /d/ {
|
||||
# root /var/www/html/GameData/;
|
||||
# }
|
||||
|
||||
# Reverse Proxy to your AquaDX.
|
||||
location /aqua/ {
|
||||
proxy_redirect off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://127.0.0.1/;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
45
AquaNet/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "aqua-net",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"lint": "eslint . --ext ts,tsx,svelte --max-warnings 0 --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/svelte": "^4.1.0",
|
||||
"@iconify/tools": "^4.1.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@unocss/svelte-scoped": "^0.65.2",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"sass": "^1.83.0",
|
||||
"shiki": "^1.24.4",
|
||||
"svelte": "^5",
|
||||
"svelte-check": "^4",
|
||||
"svelte-turnstile": "^0.9.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"unocss": "^0.65.2",
|
||||
"vite": "^6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"cal-heatmap": "^4.2.4",
|
||||
"chart.js": "^4.4.7",
|
||||
"d3": "^7.9.0",
|
||||
"lxgw-wenkai-lite-webfont": "^1.7.0",
|
||||
"modern-normalize": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"svelte5-router": "^3.0.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
|
||||
}
|
||||
4499
AquaNet/pnpm-lock.yaml
generated
Normal file
BIN
AquaNet/public/assets/email/border.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
27
AquaNet/public/assets/fonts/Quicksand.400.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.vietnamese.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
AquaNet/public/assets/fonts/Quicksand.400.latin-ext.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.400.latin.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.400.vietnamese.woff2
Normal file
27
AquaNet/public/assets/fonts/Quicksand.500.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.500.vietnamese.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.500.latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Quicksand.400.latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
AquaNet/public/assets/fonts/Quicksand.500.latin-ext.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.500.latin.woff2
Normal file
BIN
AquaNet/public/assets/fonts/Quicksand.500.vietnamese.woff2
Normal file
BIN
AquaNet/public/assets/icons/AquaDX Cat Badge.png
Normal file
|
After Width: | Height: | Size: 6.9 MiB |
BIN
AquaNet/public/assets/icons/AquaDX Cat.128px.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
AquaNet/public/assets/icons/AquaDX Cat.png
Normal file
|
After Width: | Height: | Size: 7.0 MiB |
BIN
AquaNet/public/assets/icons/AquaDX Cat.psd
Normal file
BIN
AquaNet/public/assets/icons/AquaDX Cat.webp
Normal file
|
After Width: | Height: | Size: 778 KiB |
BIN
AquaNet/public/assets/icons/Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
AquaNet/public/assets/icons/Icon.psd
Normal file
BIN
AquaNet/public/assets/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
AquaNet/public/assets/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
AquaNet/public/assets/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
9
AquaNet/public/assets/icons/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/assets/icons/mstile-150x150.png"/>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
AquaNet/public/assets/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
AquaNet/public/assets/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
AquaNet/public/assets/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
AquaNet/public/assets/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
71869
AquaNet/public/assets/icons/safari-pinned-tab.svg
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
19
AquaNet/public/assets/icons/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
AquaNet/public/assets/imgs/All Perfect.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
AquaNet/public/assets/imgs/All Perfect.psd
Normal file
BIN
AquaNet/public/assets/imgs/Full Combo.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
AquaNet/public/assets/imgs/Full Combo.psd
Normal file
BIN
AquaNet/public/assets/imgs/no_cover.jpg
Normal file
|
After Width: | Height: | Size: 894 KiB |
BIN
AquaNet/public/assets/imgs/no_cover.psd
Normal file
BIN
AquaNet/public/assets/imgs/no_profile.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
AquaNet/public/assets/imgs/no_profile.psd
Normal file
BIN
AquaNet/public/assets/meta/meta.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
101
AquaNet/src/App.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { Route, Router } from "svelte5-router";
|
||||
import Welcome from "./pages/Welcome.svelte";
|
||||
import UserHome from "./pages/UserHome.svelte";
|
||||
import Home from "./pages/Home.svelte";
|
||||
import Ranking from "./pages/Ranking.svelte";
|
||||
import { USER } from "./libs/sdk";
|
||||
import type { AquaNetUser } from "./libs/generalTypes";
|
||||
import Settings from "./pages/User/Settings.svelte";
|
||||
import { pfp } from "./libs/ui"
|
||||
|
||||
console.log(`%c
|
||||
┏━┓ ┳━┓━┓┏━
|
||||
┣━┫┏━┓┓ ┏┏━┓┃ ┃ ┣┫
|
||||
┛ ┗┗━┫┗━┻┗━┻┻━┛━┛┗━
|
||||
┗ v${APP_VERSION}`, `
|
||||
background: linear-gradient(-45deg, rgba(18,194,233,1) 0%, rgba(196,113,237,1) 50%, rgba(246,79,89,1) 100%);
|
||||
font-size: 2em;
|
||||
font-family: Monospace;
|
||||
unicode-bidi: isolate;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;`)
|
||||
|
||||
export let url = "";
|
||||
let me: AquaNetUser
|
||||
|
||||
if (USER.isLoggedIn()) USER.me().then(m => me = m).catch(e => console.error(e))
|
||||
|
||||
let path = window.location.pathname;
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
{#if path !== "/"}
|
||||
<a class="logo" href={USER.isLoggedIn() ? "/home" : "/"}>
|
||||
<img src="/assets/icons/android-chrome-192x192.png" alt="AquaDX"/>
|
||||
<span>AquaNet</span>
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/home">home</a>
|
||||
<div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
|
||||
role="button" tabindex="0">maps</div>
|
||||
<a href="/ranking">rankings</a>
|
||||
{#if me}
|
||||
<a href="/u/{me.username}">
|
||||
<img alt="profile" class="pfp" use:pfp={me}/>
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<Router {url}>
|
||||
<Route path="/" component={Welcome} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route path="/ranking" component={Ranking} />
|
||||
<Route path="/ranking/:game" component={Ranking} />
|
||||
<Route path="/u/:username" component={UserHome} />
|
||||
<Route path="/u/:username/:game" component={UserHome} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Router>
|
||||
|
||||
<style lang="sass">
|
||||
@use "vars"
|
||||
|
||||
nav
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
align-items: center
|
||||
gap: 32px
|
||||
height: vars.$nav-height
|
||||
|
||||
padding: 0 48px
|
||||
|
||||
z-index: 10
|
||||
position: relative
|
||||
|
||||
img
|
||||
width: 1.5rem
|
||||
height: 1.5rem
|
||||
border-radius: 50%
|
||||
object-fit: cover
|
||||
|
||||
.pfp
|
||||
width: 2rem
|
||||
height: 2rem
|
||||
|
||||
.logo
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 8px
|
||||
font-weight: bold
|
||||
color: vars.$c-main
|
||||
letter-spacing: 0.2em
|
||||
flex: 1
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
> span
|
||||
display: none
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
justify-content: center
|
||||
|
||||
</style>
|
||||
349
AquaNet/src/app.sass
Normal file
@@ -0,0 +1,349 @@
|
||||
@use "sass:color"
|
||||
@use "vars"
|
||||
@import 'lxgw-wenkai-lite-webfont/style.css'
|
||||
|
||||
html
|
||||
height: 100%
|
||||
|
||||
body
|
||||
font-family: vars.$font
|
||||
line-height: 1.5
|
||||
font-weight: 400
|
||||
width: 100%
|
||||
height: 100%
|
||||
margin: 0
|
||||
overflow-x: hidden
|
||||
|
||||
color-scheme: dark
|
||||
color: rgba(255, 255, 255, 0.87)
|
||||
background-color: vars.$c-bg
|
||||
|
||||
font-synthesis: none
|
||||
text-rendering: optimizeLegibility
|
||||
-webkit-font-smoothing: antialiased
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
|
||||
a
|
||||
font-weight: 500
|
||||
color: vars.$c-main
|
||||
text-decoration: inherit
|
||||
|
||||
a:hover
|
||||
color: vars.$c-main
|
||||
|
||||
h1
|
||||
font-size: 3.2em
|
||||
line-height: 1.1
|
||||
|
||||
|
||||
.card
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
border-radius: vars.$border-radius
|
||||
padding: 12px 16px
|
||||
background: vars.$ov-light
|
||||
|
||||
|
||||
blockquote
|
||||
$c1: rgba(255, 149, 149, 0.05)
|
||||
$c2: rgba(255, 152, 152, 0.12)
|
||||
background: repeating-linear-gradient(45deg, $c1, $c1 10px, $c2 10px, $c2 20px)
|
||||
padding: 10px 20px 10px 20px
|
||||
margin: 16px 0
|
||||
border-left: solid #ff7c7c 3px
|
||||
border-radius: vars.$border-radius
|
||||
|
||||
|
||||
#app
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
|
||||
main:not(.no-margin)
|
||||
max-width: 1280px
|
||||
|
||||
margin: 0 auto
|
||||
padding-bottom: 100px
|
||||
|
||||
|
||||
button
|
||||
border-radius: vars.$border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: vars.$ov-lighter
|
||||
opacity: 0.9
|
||||
cursor: pointer
|
||||
transition: vars.$transition
|
||||
|
||||
button:hover
|
||||
border-color: vars.$c-main
|
||||
|
||||
button:focus, button:focus-visible
|
||||
color: vars.$c-main
|
||||
outline: none
|
||||
|
||||
button.error
|
||||
color: unset
|
||||
&:hover
|
||||
border-color: vars.$c-error
|
||||
color: vars.$c-error
|
||||
//background: $c-error
|
||||
//border-color: transparent
|
||||
|
||||
button.icon
|
||||
padding: 0.6em
|
||||
font-size: 1.2em
|
||||
border-radius: 50px
|
||||
@extend .flex-center
|
||||
|
||||
.level-0
|
||||
//--lv-color: #6ED43E
|
||||
--lv-color: 110, 212, 62
|
||||
|
||||
.level-1
|
||||
//--lv-color: #F7B807
|
||||
--lv-color: 247, 184, 7
|
||||
|
||||
.level-2
|
||||
//--lv-color: #FF828D
|
||||
--lv-color: 255, 130, 141
|
||||
|
||||
.level-3
|
||||
//--lv-color: #A051DC
|
||||
--lv-color: 160, 81, 220
|
||||
|
||||
.level-4
|
||||
//--lv-color: #c299e7
|
||||
--lv-color: 194, 153, 231
|
||||
|
||||
.level-5
|
||||
// World's End for chunithm, or Utage for maimai
|
||||
// --lv-color: #eff2e1
|
||||
// --lv-color: 239, 242, 225
|
||||
--lv-text-clip: linear-gradient(110deg, #5ac42c, #5ccc22, #959f26, #cc7c23, #c93143, #8f4876, #4c3eb1, #3c3397)
|
||||
|
||||
|
||||
.error
|
||||
color: vars.$c-error
|
||||
|
||||
input
|
||||
border-radius: vars.$border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: vars.$ov-lighter
|
||||
transition: vars.$transition
|
||||
box-sizing: border-box
|
||||
|
||||
// Dropdown
|
||||
select
|
||||
border-radius: vars.$border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: vars.$ov-lighter
|
||||
transition: vars.$transition
|
||||
box-sizing: border-box
|
||||
|
||||
option
|
||||
background-color: #333
|
||||
color: #fff
|
||||
|
||||
option:hover
|
||||
background-color: #555
|
||||
|
||||
input[type="checkbox"]
|
||||
width: 1.2em
|
||||
height: 1.2em
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 1px solid vars.$c-main
|
||||
background-color: vars.$ov-lighter
|
||||
appearance: none
|
||||
cursor: pointer
|
||||
flex-shrink: 0
|
||||
|
||||
&:checked
|
||||
background-color: vars.$c-main
|
||||
border-color: vars.$c-main
|
||||
|
||||
label
|
||||
cursor: pointer
|
||||
|
||||
input:focus, input:focus-visible
|
||||
border: 1px solid vars.$c-main
|
||||
outline: none
|
||||
|
||||
input.error
|
||||
border: 1px solid vars.$c-error
|
||||
|
||||
.flex-center
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
|
||||
.inline-flex-center
|
||||
display: inline-flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
.clickable
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
|
||||
|
||||
// Content containers
|
||||
.content-main
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 20px
|
||||
margin: 100px auto 0
|
||||
padding: 32px 32px 128px
|
||||
min-height: 100%
|
||||
max-width: vars.$w-max
|
||||
|
||||
background-color: color.adjust(vars.$c-bg, $lightness: -3%)
|
||||
border-radius: 16px 16px 0 0
|
||||
|
||||
@media (max-width: #{vars.$w-max + (64px) * 2})
|
||||
margin: 100px 32px 0
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
margin: 100px 0 0
|
||||
|
||||
.fw-block
|
||||
margin-left: -32px
|
||||
margin-right: -32px
|
||||
padding: 12px 32px
|
||||
background-color: vars.$ov-darker
|
||||
// Inner shadow
|
||||
box-shadow: inset 0 10px 10px -2px vars.$c-shadow, inset 0 -10px 10px -2px vars.$c-shadow
|
||||
|
||||
> h2.outer-title, > .outer-title-options
|
||||
margin-top: -5rem
|
||||
margin-bottom: 1rem
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
text-align: center
|
||||
|
||||
> .outer-title-options
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
|
||||
nav
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 10px
|
||||
top: 4px
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
flex-direction: column
|
||||
|
||||
> h2, > .outer-title-options > h2
|
||||
margin: 0
|
||||
|
||||
main.content
|
||||
@extend .content-main
|
||||
|
||||
// Not used. still need a lot of work
|
||||
.content-popup
|
||||
position: absolute
|
||||
inset: 0
|
||||
|
||||
> div
|
||||
@extend .content-main
|
||||
position: absolute
|
||||
inset: 0
|
||||
top: 100px
|
||||
background: rgba(color.adjust(vars.$c-bg, $lightness: -3%), 0.9)
|
||||
backdrop-filter: blur(5px)
|
||||
box-shadow: 0 0 10px 6px rgba(black, 0.4)
|
||||
max-width: calc(vars.$w-max + 20px)
|
||||
|
||||
@media (max-width: #{vars.$w-max + (64px) * 2})
|
||||
margin: 100px 22px 0
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
margin: 100px 0 0
|
||||
|
||||
|
||||
|
||||
// Overlay
|
||||
.overlay
|
||||
position: fixed
|
||||
inset: 0
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
z-index: 1000
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
h2, p
|
||||
user-select: none
|
||||
margin: 0
|
||||
|
||||
> div
|
||||
background-color: vars.$c-bg
|
||||
padding: 2rem
|
||||
border-radius: vars.$border-radius
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
max-width: 400px
|
||||
|
||||
.no-margin
|
||||
margin: 0
|
||||
|
||||
nav
|
||||
> div, > a
|
||||
cursor: pointer
|
||||
transition: vars.$transition
|
||||
text-decoration: underline 1px solid transparent
|
||||
text-underline-offset: 0.1em
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
color: unset
|
||||
font-weight: unset
|
||||
|
||||
&:hover
|
||||
color: vars.$c-main
|
||||
text-decoration-color: vars.$c-main
|
||||
text-underline-offset: 0.5em
|
||||
|
||||
&.active
|
||||
color: vars.$c-main
|
||||
|
||||
|
||||
.hide-scrollbar
|
||||
&::-webkit-scrollbar
|
||||
display: none
|
||||
-ms-overflow-style: none
|
||||
scrollbar-width: none
|
||||
|
||||
|
||||
.aqua-tooltip
|
||||
box-shadow: 0 0 5px 0 vars.$c-shadow
|
||||
border-radius: vars.$border-radius
|
||||
position: absolute
|
||||
padding: 4px 8px
|
||||
background: vars.$ov-lighter
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
|
||||
76
AquaNet/src/components/ActionCard.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
export let color: string = '179, 198, 255'
|
||||
export let icon: string
|
||||
|
||||
// Manually positioned icons
|
||||
const iconPos = [
|
||||
[1, 0.5, 2],
|
||||
[6, 2, 1.5],
|
||||
[-0.5, 4.5, 1.3],
|
||||
[5, -0.5],
|
||||
[3.5, 4.5],
|
||||
[9.5, 0.3, 1.2],
|
||||
[12.5, 2.5, 0.8],
|
||||
[10, 4.4, 0.8],
|
||||
]
|
||||
</script>
|
||||
|
||||
<div class="action-card" style="--card-color: {color}" on:click role="button" tabindex="0" on:keydown>
|
||||
<slot/>
|
||||
|
||||
<div class="icons">
|
||||
{#each iconPos as [x, y, size], i}
|
||||
<Icon icon={icon} style={`top: ${y}rem; right: ${x}rem; font-size: ${size || 1}em`} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@use '../vars'
|
||||
|
||||
.action-card
|
||||
overflow: hidden
|
||||
padding: 1rem
|
||||
border-radius: vars.$border-radius
|
||||
box-shadow: 0 5px 5px 1px vars.$c-shadow
|
||||
transition: all 0.2s ease
|
||||
cursor: pointer
|
||||
position: relative
|
||||
background: linear-gradient(45deg, transparent 20%, rgba(var(--card-color), 0.5) 100%)
|
||||
outline: 1px solid transparent
|
||||
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0))
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 0 0.5rem 0.2rem vars.$c-shadow
|
||||
transform: translateY(-3px)
|
||||
|
||||
// Drop shadow glow
|
||||
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0.5))
|
||||
outline-color: rgba(var(--card-color), 0.5)
|
||||
|
||||
:global(span)
|
||||
font-size: 1.2rem
|
||||
display: block
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
.icons
|
||||
position: absolute
|
||||
inset: 0
|
||||
color: rgba(var(--card-color), 0.5)
|
||||
font-size: 2rem
|
||||
transition: all 0.2s ease
|
||||
z-index: -1
|
||||
mask-image: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.5) 70%, white 100%)
|
||||
opacity: 0.8
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
opacity: 0.6
|
||||
|
||||
:global(> svg)
|
||||
position: absolute
|
||||
rotate: 20deg
|
||||
</style>
|
||||
62
AquaNet/src/components/CommunityCard.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
export let color: string = '179, 198, 255'
|
||||
export let icon: string
|
||||
|
||||
</script>
|
||||
|
||||
<div class="action-card" style="--card-color: {color}" on:click role="button" tabindex="0" on:keydown>
|
||||
<slot/>
|
||||
|
||||
<div class="icon">
|
||||
<Icon icon={icon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@use '../vars'
|
||||
|
||||
.action-card
|
||||
overflow: hidden
|
||||
padding: 1rem
|
||||
border-radius: vars.$border-radius
|
||||
box-shadow: 0 5px 5px 1px vars.$c-shadow
|
||||
transition: all 0.2s ease
|
||||
cursor: pointer
|
||||
position: relative
|
||||
background: linear-gradient(45deg, transparent 20%, rgba(var(--card-color), 0.5) 100%)
|
||||
outline: 1px solid transparent
|
||||
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0))
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 0 0.5rem 0.2rem vars.$c-shadow
|
||||
transform: translateY(-3px)
|
||||
|
||||
// Drop shadow glow
|
||||
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0.5))
|
||||
outline-color: rgba(var(--card-color), 0.5)
|
||||
|
||||
:global(span)
|
||||
font-size: 1.2rem
|
||||
display: block
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
.icon
|
||||
position: absolute
|
||||
display: flex
|
||||
color: rgba(var(--card-color), 0.5)
|
||||
font-size: 4rem
|
||||
right: 0
|
||||
bottom: 0
|
||||
padding: .5rem
|
||||
transition: all 0.2s ease
|
||||
z-index: -1
|
||||
mask-image: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.5) 70%, white 100%)
|
||||
opacity: 0.8
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
opacity: 0.6
|
||||
</style>
|
||||
64
AquaNet/src/components/RankDetails.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { t } from "../libs/i18n";
|
||||
import type { GenericGameSummary } from "../libs/generalTypes";
|
||||
|
||||
export let g: GenericGameSummary
|
||||
|
||||
const detail = Object.entries(g.detailedRanks).toSorted((a, b) => +b[0] - +a[0])
|
||||
</script>
|
||||
|
||||
<div class="rank-detail-container fw-block" transition:slide>
|
||||
<div>
|
||||
<h2>{t("UserHome.RankDetail.Title")}</h2>
|
||||
<table><tbody>
|
||||
<!-- rankDetails: { Level : { Rank : Count } } -->
|
||||
<!-- Rows are levels, columns are ranks -->
|
||||
|
||||
<!-- Headers -->
|
||||
<tr>
|
||||
<th>{t("UserHome.RankDetail.Level")}</th>
|
||||
{#each Object.values(g.ranks) as rankMap}<th>{rankMap.name}</th>{/each}
|
||||
</tr>
|
||||
|
||||
<!-- Data -->
|
||||
{#each detail as [level, rankMap]}
|
||||
<tr>
|
||||
<td>{level}</td>
|
||||
{#each Object.values(rankMap) as count}<td>{count}</td>{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.rank-detail-container
|
||||
> div
|
||||
margin: 1em auto
|
||||
max-width: 500px
|
||||
|
||||
table
|
||||
width: 100%
|
||||
border-collapse: collapse
|
||||
table-layout: fixed
|
||||
|
||||
th:not(:first-child)
|
||||
background: vars.$grad-special
|
||||
-webkit-background-clip: text
|
||||
-webkit-text-fill-color: transparent
|
||||
background-clip: text
|
||||
color: vars.$c-main
|
||||
padding: 0.5em
|
||||
|
||||
th, td
|
||||
padding: 0.5em
|
||||
text-align: center
|
||||
|
||||
&:first-child
|
||||
color: vars.$c-main
|
||||
</style>
|
||||
143
AquaNet/src/components/RatingCompSong.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { t } from "../libs/i18n";
|
||||
import { type GameName, type ParsedComposition, roundFloor } from "../libs/scoring";
|
||||
import { coverNotFound } from "../libs/ui";
|
||||
import { tooltip } from "../libs/ui";
|
||||
import useLocalStorage from "../libs/hooks/useLocalStorage.svelte";
|
||||
|
||||
export let game: GameName
|
||||
export let p: ParsedComposition
|
||||
const rounding = useLocalStorage("rounding", true)
|
||||
</script>
|
||||
|
||||
<div class="map-detail-container" transition:slide>
|
||||
<div class="scores">
|
||||
<div>
|
||||
<img src={p.img} alt="" on:error={coverNotFound} />
|
||||
<div class="info">
|
||||
<div class="first-line">
|
||||
<div class="song-title">{p.name ?? t("UserHome.UnknownSong")}</div>
|
||||
<span class={`lv level-${p.diffId === 10 ? 3 : p.diffId}`}>
|
||||
{ p.difficulty ?? '-' }
|
||||
</span>
|
||||
</div>
|
||||
<div class="second-line">
|
||||
<span class={`rank-${p.rank[0]}`}>
|
||||
<span class="rank-text">{p.rank.replace("p", "+")}</span>
|
||||
<span class="rank-num" use:tooltip={(p.score / 10000).toFixed(4)}>
|
||||
{rounding.value ? roundFloor(p.score, game, 1) : (p.score / 10000).toFixed(4)}%
|
||||
</span>
|
||||
</span>
|
||||
{#if p.ratingChange !== undefined}
|
||||
<span class="dx-change">{ p.ratingChange }</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@use "../vars"
|
||||
vars.$gap: 20px
|
||||
|
||||
.map-detail-container
|
||||
background-color: rgb(35,35,35)
|
||||
border-radius: vars.$border-radius
|
||||
overflow: hidden
|
||||
|
||||
.scores
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-wrap: wrap
|
||||
gap: vars.$gap
|
||||
|
||||
// Image and song info
|
||||
> div
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 12px
|
||||
max-width: 100%
|
||||
box-sizing: border-box
|
||||
|
||||
img
|
||||
width: 50px
|
||||
height: 50px
|
||||
border-radius: vars.$border-radius
|
||||
object-fit: cover
|
||||
|
||||
// Song info and score
|
||||
> div.info
|
||||
flex: 1
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
overflow: hidden
|
||||
flex-direction: column
|
||||
|
||||
.first-line
|
||||
display: flex
|
||||
flex-direction: row
|
||||
|
||||
// Limit song name to one line
|
||||
.song-title
|
||||
flex: 1
|
||||
min-width: 0
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
// Make song score and rank not wrap
|
||||
> div:last-child
|
||||
white-space: nowrap
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
|
||||
.rank-text
|
||||
text-align: left
|
||||
|
||||
.rank-S
|
||||
// Gold green gradient on text
|
||||
background: vars.$grad-special
|
||||
-webkit-background-clip: text
|
||||
color: transparent
|
||||
|
||||
.rank-A
|
||||
color: #ff8a8a
|
||||
|
||||
.rank-B
|
||||
color: #6ba6ff
|
||||
|
||||
.lv
|
||||
width: 30px
|
||||
text-align: center
|
||||
background: rgba(var(--lv-color), 0.6)
|
||||
padding: 0 6px
|
||||
border-radius: 0 vars.$border-radius 0 vars.$border-radius
|
||||
|
||||
// Inset shadow, like it's a paper below this card with a cut
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,0.5)
|
||||
|
||||
span
|
||||
display: inline-block
|
||||
text-align: left
|
||||
|
||||
.second-line
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
|
||||
// Vertical table-like alignment
|
||||
span.rank-text
|
||||
min-width: 40px
|
||||
span.rank-num
|
||||
min-width: 60px
|
||||
span.dx-change
|
||||
margin-right: 0.5rem
|
||||
color: vars.$c-good
|
||||
</style>
|
||||
42
AquaNet/src/components/RatingComposition.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import RatingCompSong from "./RatingCompSong.svelte";
|
||||
import { parseComposition, type GameName } from "../libs/scoring";
|
||||
import { type MusicMeta } from "../libs/generalTypes";
|
||||
|
||||
export let title: string;
|
||||
export let comp: string | undefined;
|
||||
export let allMusics: Record<string, MusicMeta>;
|
||||
export let game: GameName;
|
||||
export let top: number | undefined = undefined;
|
||||
|
||||
let split = comp?.split(",")?.filter(it => it.split(":")[0] !== '0')
|
||||
?.map(it => parseComposition(it, allMusics, game))
|
||||
|
||||
if (top) split = split?.toSorted((a, b) => b.score - a.score).slice(0, top)
|
||||
if (split) console.log("Split", split)
|
||||
</script>
|
||||
|
||||
{#if split && comp}
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
<div class="rating-composition">
|
||||
{#each split as p}
|
||||
<div>
|
||||
<RatingCompSong {p} {game}/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.rating-composition
|
||||
display: grid
|
||||
// 3 columns
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))
|
||||
gap: vars.$gap
|
||||
</style>
|
||||
87
AquaNet/src/components/StatusOverlays.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { ConfirmProps } from "../libs/generalTypes";
|
||||
import { DISCORD_INVITE } from "../libs/config";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { t } from "../libs/i18n"
|
||||
|
||||
// Props
|
||||
export let confirm: ConfirmProps | null = null
|
||||
export let error: string | null
|
||||
export let loading: boolean = false
|
||||
</script>
|
||||
|
||||
{#if confirm}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2>{confirm.title}</h2>
|
||||
<span>{confirm.message}</span>
|
||||
|
||||
<div class="actions">
|
||||
{#if confirm.cancel}
|
||||
<!-- Svelte LSP is very annoying here -->
|
||||
<button on:click={() => {
|
||||
confirm && confirm.cancel && confirm.cancel()
|
||||
|
||||
// Set to null
|
||||
confirm = null
|
||||
}}>{t('action.cancel')}</button>
|
||||
{/if}
|
||||
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2 class="error">{t('status.error')}</h2>
|
||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||
<span>{t('status.detail', { detail: error })}</span>
|
||||
|
||||
<div class="actions">
|
||||
<button on:click={() => location.reload()} class="error">
|
||||
{t('action.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading && !error}
|
||||
<div class="overlay loading" transition:fade>
|
||||
<Icon class="icon" icon="svg-spinners:pulse-2"/>
|
||||
<span><span>LOADING</span></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
.actions
|
||||
display: flex
|
||||
gap: 16px
|
||||
|
||||
button
|
||||
width: 100%
|
||||
|
||||
.loading.overlay
|
||||
font-size: 28rem
|
||||
|
||||
:global(.icon)
|
||||
opacity: 0.5
|
||||
|
||||
> span
|
||||
position: absolute
|
||||
inset: 0
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
background: transparent
|
||||
|
||||
letter-spacing: 20px
|
||||
margin-left: 20px
|
||||
|
||||
font-size: 1.5rem
|
||||
</style>
|
||||
76
AquaNet/src/components/Tooltip.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
|
||||
export let triggeredBy: string
|
||||
export let loading: boolean = false
|
||||
let isHovered = false
|
||||
let x: number, y: number
|
||||
let targets: Element[] = []
|
||||
|
||||
onMount(() => {
|
||||
targets = [...document.querySelectorAll(triggeredBy)]
|
||||
targets.forEach((el) => {
|
||||
el.addEventListener('mouseover', mouseOver)
|
||||
el.addEventListener('mousemove', mouseMove)
|
||||
el.addEventListener('mouseleave', mouseLeave)
|
||||
})
|
||||
if (targets.length === 0) {
|
||||
console.warn(`No elements found with selector "${triggeredBy}"`)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
targets.forEach((el) => {
|
||||
el.removeEventListener('mouseover', mouseOver)
|
||||
el.removeEventListener('mousemove', mouseMove)
|
||||
el.removeEventListener('mouseleave', mouseLeave)
|
||||
})
|
||||
})
|
||||
|
||||
function mouseOver(event: MouseEvent) {
|
||||
console.log('over')
|
||||
isHovered = true
|
||||
updatePosition(event)
|
||||
}
|
||||
|
||||
function mouseMove(event: MouseEvent) {
|
||||
console.log('move')
|
||||
updatePosition(event)
|
||||
}
|
||||
|
||||
function updatePosition(event: MouseEvent) {
|
||||
x = event.pageX + 5
|
||||
y = event.pageY + 20
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
console.log('leave')
|
||||
isHovered = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isHovered}
|
||||
<div style="top: {y}px; left: {x}px" class="tooltip" class:loading>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.tooltip
|
||||
position: absolute
|
||||
z-index: 1000
|
||||
background: white
|
||||
padding: 10px 16px
|
||||
border-radius: vars.$border-radius
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1)
|
||||
pointer-events: none
|
||||
white-space: nowrap
|
||||
color: #242424
|
||||
transform: translate(-50%, 0)
|
||||
transition: opacity 0.2s
|
||||
|
||||
&.loading
|
||||
opacity: 0
|
||||
</style>
|
||||
56
AquaNet/src/components/UserCard.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import type { GenericGameSummary } from "../libs/generalTypes";
|
||||
import { GAME } from "../libs/sdk";
|
||||
import type { GameName } from "../libs/scoring";
|
||||
import { pfp, pfpNotFound } from "../libs/ui";
|
||||
|
||||
export let username: string
|
||||
export let game: GameName
|
||||
export let setLoading: (loading: boolean) => void = () => {}
|
||||
|
||||
let data: GenericGameSummary
|
||||
let error = ""
|
||||
|
||||
setLoading(true)
|
||||
GAME.userSummary(username, game).then(d => data = d).catch(e => error = e).finally(_ => setLoading(false))
|
||||
</script>
|
||||
|
||||
{#if !data}
|
||||
<div>Loading...</div>
|
||||
{:else if error}
|
||||
<div>Error: {error}</div>
|
||||
{:else}
|
||||
<div class="user-card">
|
||||
<img use:pfp={data.aquaUser} alt="Profile" />
|
||||
<div class="details">
|
||||
<span class="in-game-name">{data.name}</span>
|
||||
<span class="username">@{username}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.user-card
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: vars.$gap
|
||||
|
||||
.details
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
|
||||
.username
|
||||
font-size: 0.8em
|
||||
|
||||
img
|
||||
width: 50px
|
||||
height: 50px
|
||||
border-radius: 50%
|
||||
object-fit: cover
|
||||
object-position: center
|
||||
</style>
|
||||
29
AquaNet/src/components/chart/Line.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Chart, Tooltip, type ChartData, type ChartOptions } from 'chart.js';
|
||||
import type { HTMLCanvasAttributes } from 'svelte/elements';
|
||||
import 'chart.js/auto';
|
||||
|
||||
interface Props extends HTMLCanvasAttributes {
|
||||
data: ChartData<'line', any, string>
|
||||
options: ChartOptions<'line'>
|
||||
}
|
||||
const { data, options, ...rest }: Props = $props()
|
||||
|
||||
Chart.register(Tooltip)
|
||||
|
||||
let canvasElem: HTMLCanvasElement
|
||||
let chart: Chart
|
||||
|
||||
$effect(() => {
|
||||
chart = new Chart(canvasElem, { type: 'line', data, options })
|
||||
return () => chart.destroy()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!chart) return
|
||||
chart.data = data
|
||||
chart.update()
|
||||
})
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvasElem} {...rest}></canvas>
|
||||
205
AquaNet/src/components/settings/ChuniSettings.svelte
Normal file
@@ -0,0 +1,205 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
type AquaNetUser,
|
||||
type UserBox,
|
||||
type UserItem,
|
||||
} from "../../libs/generalTypes";
|
||||
import { DATA, USER, USERBOX } from "../../libs/sdk";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import { DATA_HOST, FADE_IN, FADE_OUT, HAS_USERBOX_ASSETS } from "../../libs/config";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import GameSettingFields from "./GameSettingFields.svelte";
|
||||
import { filter } from "d3";
|
||||
import { coverNotFound } from "../../libs/ui";
|
||||
|
||||
let user: AquaNetUser
|
||||
let [loading, error, submitting, preview] = [true, "", "", ""]
|
||||
let changed: string[] = [];
|
||||
|
||||
// Available (unlocked) options for each kind of item
|
||||
// In allItems: 'namePlate', 'frame', 'trophy', 'mapIcon', 'systemVoice', 'avatarAccessory'
|
||||
let allItems: Record<string, Record<string, { name: string }>> = {}
|
||||
let iKinds = { namePlate: 1, frame: 2, trophy: 3, mapIcon: 8, systemVoice: 9, avatarAccessory: 11 }
|
||||
// In userbox: 'nameplateId', 'frameId', 'trophyId', 'mapIconId', 'voiceId', 'avatar{Wear/Head/Face/Skin/Item/Front/Back}'
|
||||
let userbox: UserBox
|
||||
let avatarKinds = ['Wear', 'Head', 'Face', 'Skin', 'Item', 'Front', 'Back']
|
||||
// iKey should match allItems keys, and ubKey should match userbox keys
|
||||
let userItems: { iKey: string, ubKey: keyof UserBox, items: UserItem[] }[] = []
|
||||
|
||||
// Submit changes
|
||||
function submit(field: keyof UserBox) {
|
||||
let obj = { field, value: userbox[field] }
|
||||
if (submitting) return
|
||||
submitting = obj.field
|
||||
|
||||
USERBOX.setUserBox(obj)
|
||||
.then(() => changed = changed.filter((c) => c !== obj.field))
|
||||
.catch(e => error = e.message)
|
||||
.finally(() => submitting = "")
|
||||
}
|
||||
|
||||
// Fetch data from the server
|
||||
async function fetchData() {
|
||||
const profile = await USERBOX.getProfile().catch(_ => {
|
||||
loading = false
|
||||
error = t("userbox.error.nodata")
|
||||
})
|
||||
if (!profile) return
|
||||
userbox = profile.user
|
||||
userItems = Object.entries(iKinds).flatMap(([iKey, iKind]) => {
|
||||
if (iKey != 'avatarAccessory') {
|
||||
let ubKey = `${iKey}Id`
|
||||
if (ubKey == 'namePlateId') ubKey = 'nameplateId'
|
||||
if (ubKey == 'systemVoiceId') ubKey = 'voiceId'
|
||||
return [{ iKey, ubKey: ubKey as keyof UserBox,
|
||||
items: profile.items.filter(x => x.itemKind === iKind)
|
||||
}]
|
||||
}
|
||||
|
||||
return avatarKinds.map((aKind, i) => {
|
||||
let items = profile.items.filter(x => x.itemKind === iKind && Math.floor(x.itemId / 100000) % 10 === i + 1)
|
||||
return { iKey, ubKey: `avatar${aKind}` as keyof UserBox, items }
|
||||
})
|
||||
})
|
||||
|
||||
allItems = await DATA.allItems('chu3').catch(_ => {
|
||||
loading = false
|
||||
error = t("userbox.error.nodata")
|
||||
}) as typeof allItems
|
||||
|
||||
console.log("User Items", userItems)
|
||||
console.log("All items", allItems)
|
||||
console.log("Userbox", userbox)
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
USER.me().then(u => {
|
||||
if (!u) throw new Error(t("userbox.error.nodata"))
|
||||
user = u
|
||||
return fetchData()
|
||||
}).catch((e) => { loading = false; error = e.message });
|
||||
</script>
|
||||
|
||||
<StatusOverlays {error} loading={loading || !!submitting} />
|
||||
{#if !loading && !error}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<h2>{t("userbox.header.general")}</h2>
|
||||
<GameSettingFields game="chu3"/>
|
||||
<h2>{t("userbox.header.userbox")}</h2>
|
||||
<div class="fields">
|
||||
{#each userItems as { iKey, ubKey, items }, i}
|
||||
<div class="field">
|
||||
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
|
||||
<div>
|
||||
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
|
||||
{#each items as option}
|
||||
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if changed.includes(ubKey)}
|
||||
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
|
||||
{t("settings.profile.save")}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if HAS_USERBOX_ASSETS}
|
||||
<h2>{t("userbox.header.preview")}</h2>
|
||||
<p class="notice">{t("userbox.preview.notice")}</p>
|
||||
<input bind:value={preview} placeholder={t("userbox.preview.url")}/>
|
||||
{#if preview}
|
||||
<div class="preview">
|
||||
{#each userItems.filter(v => v.iKey != 'trophy' && v.iKey != 'systemVoice') as { iKey, ubKey, items }, i}
|
||||
<div>
|
||||
<span>{ts(`userbox.${ubKey}`)}</span>
|
||||
<img src={`${preview}/${iKey}/${userbox[ubKey].toString().padStart(8, '0')}.png`} alt="" on:error={coverNotFound} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@use "../../vars"
|
||||
|
||||
input
|
||||
width: 100%
|
||||
|
||||
h2
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
p.notice
|
||||
opacity: 0.6
|
||||
margin-top: 0
|
||||
|
||||
.preview
|
||||
margin-top: 32px
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
justify-content: space-between
|
||||
gap: 32px
|
||||
|
||||
> div
|
||||
position: relative
|
||||
width: 100px
|
||||
height: 100px
|
||||
overflow: hidden
|
||||
background: vars.$ov-lighter
|
||||
border-radius: vars.$border-radius
|
||||
|
||||
span
|
||||
position: absolute
|
||||
bottom: 0
|
||||
width: 100%
|
||||
text-align: center
|
||||
z-index: 10
|
||||
background: rgba(0, 0, 0, 0.2)
|
||||
backdrop-filter: blur(2px)
|
||||
|
||||
img
|
||||
position: absolute
|
||||
inset: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: contain
|
||||
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
width: 100%
|
||||
flex-grow: 0
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
select
|
||||
width: 100%
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
width: 100%
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> select
|
||||
flex: 1
|
||||
</style>
|
||||
76
AquaNet/src/components/settings/GameSettingFields.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { SETTING } from "../../libs/sdk";
|
||||
import type { GameOption } from "../../libs/generalTypes";
|
||||
import { ts } from "../../libs/i18n";
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
import InputWithButton from "../ui/InputWithButton.svelte";
|
||||
|
||||
export let game: string;
|
||||
let gameFields: GameOption[] = []
|
||||
let submitting = ""
|
||||
let error: string;
|
||||
|
||||
SETTING.get().then(s => {
|
||||
gameFields = s.filter(it => it.game === game)
|
||||
})
|
||||
|
||||
async function submitGameOption(field: string, value: any) {
|
||||
if (submitting) return false
|
||||
submitting = field
|
||||
|
||||
await SETTING.set(field, value).catch(e => error = e.message).finally(() => submitting = "")
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fields">
|
||||
{#each gameFields as field}
|
||||
<div class="field {field.type.toLowerCase()}">
|
||||
{#if field.type === "Boolean"}
|
||||
<input id={field.key} type="checkbox" bind:checked={field.value}
|
||||
on:change={() => submitGameOption(field.key, field.value)}/>
|
||||
<label for={field.key}>
|
||||
<span class="name">{ts(`settings.fields.${field.key}.name`)}</span>
|
||||
<span class="desc">{ts(`settings.fields.${field.key}.desc`)}</span>
|
||||
</label>
|
||||
{/if}
|
||||
{#if field.type === "String"}
|
||||
<label for={field.key}>
|
||||
<span class="name">{ts(`settings.fields.${field.key}.name`)}</span>
|
||||
<span class="desc">{ts(`settings.fields.${field.key}.desc`)}</span>
|
||||
</label>
|
||||
<InputWithButton bind:field={field} callback={() => submitGameOption(field.key, field.value)}/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<StatusOverlays {error} loading={!gameFields.length || !!submitting}/>
|
||||
|
||||
<style lang="sass">
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
|
||||
.field.string
|
||||
flex-direction: column
|
||||
align-items: flex-start
|
||||
gap: 0.5rem
|
||||
|
||||
.field.boolean
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
|
||||
.field
|
||||
display: flex
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
max-width: max-content
|
||||
|
||||
.desc
|
||||
opacity: 0.6
|
||||
</style>
|
||||
57
AquaNet/src/components/settings/GeneralGameSettings.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import { fade } from "svelte/transition";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import GameSettingFields from "./GameSettingFields.svelte";
|
||||
import { ts } from "../../libs/i18n";
|
||||
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
|
||||
|
||||
const rounding = useLocalStorage("rounding", true);
|
||||
</script>
|
||||
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
||||
<GameSettingFields game="general"/>
|
||||
<div class="field">
|
||||
<div class="bool">
|
||||
<input id="rounding" type="checkbox" bind:checked={rounding.value}/>
|
||||
<label for="rounding">
|
||||
<span class="name">{ts(`settings.fields.rounding.name`)}</span>
|
||||
<span class="desc">{ts(`settings.fields.rounding.desc`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
|
||||
.bool
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.desc
|
||||
opacity: 0.6
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> input
|
||||
flex: 1
|
||||
</style>
|
||||
110
AquaNet/src/components/settings/Mai2Settings.svelte
Normal file
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import { t } from "../../libs/i18n.js";
|
||||
import Icon from "@iconify/svelte";
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
import { GAME } from "../../libs/sdk";
|
||||
|
||||
const profileFields = [
|
||||
['name', t('settings.mai2.name')],
|
||||
]
|
||||
|
||||
export let username: string;
|
||||
let error: string
|
||||
let submitting = ""
|
||||
let values = Array(profileFields.length).fill('')
|
||||
let changed: string[] = []
|
||||
|
||||
GAME.userSummary(username, 'mai2').then(({name}) => {
|
||||
values = [name]
|
||||
}).catch(e => error = e.message)
|
||||
|
||||
function submit(field: string, value: string) {
|
||||
if (submitting) return
|
||||
submitting = field
|
||||
|
||||
switch (field) {
|
||||
case 'name':
|
||||
GAME.changeName('mai2', value).then(({newName}) => {
|
||||
changed = changed.filter(c => c !== field)
|
||||
values = [newName]
|
||||
}).catch(e => error = e.message).finally(() => submitting = "")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
submitting = "export"
|
||||
GAME.export('mai2')
|
||||
.then(data => download(JSON.stringify(data), `AquaDX_maimai2_export_${values[0]}.json`))
|
||||
.catch(e => error = e.message)
|
||||
.finally(() => submitting = "")
|
||||
}
|
||||
|
||||
function download(data: string, filename: string) {
|
||||
const blob = new Blob([data]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fields" out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
{#each profileFields as [field, name], i (field)}
|
||||
<div class="field">
|
||||
<label for={field}>{name}</label>
|
||||
<div>
|
||||
<input id={field} type="text"
|
||||
bind:value={values[i]} on:input={() => changed = [...changed, field]}
|
||||
placeholder={field === 'password' ? t('settings.profile.unchanged') : t('settings.profile.unset')}/>
|
||||
{#if changed.includes(field) && values[i]}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, values[i])}>
|
||||
{#if submitting === field}
|
||||
<Icon icon="line-md:loading-twotone-loop"/>
|
||||
{:else}
|
||||
{t('settings.profile.save')}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="exportButton" on:click={exportData}>
|
||||
<Icon icon="bxs:file-export"/>
|
||||
{t('settings.export')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<StatusOverlays {error} loading={!values[0] || !!submitting}/>
|
||||
|
||||
<style lang="sass">
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> input
|
||||
flex: 1
|
||||
|
||||
.exportButton
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
gap: 5px
|
||||
</style>
|
||||
9
AquaNet/src/components/settings/WaccaSettings.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import { fade } from "svelte/transition";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import GameSettingFields from "./GameSettingFields.svelte";
|
||||
</script>
|
||||
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<GameSettingFields game="wacca"/>
|
||||
</div>
|
||||
29
AquaNet/src/components/ui/InputWithButton.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { ts } from "../../libs/i18n";
|
||||
|
||||
export let field: {key: string, value: string, changed?: boolean};
|
||||
export let callback: () => Promise<boolean>;
|
||||
</script>
|
||||
|
||||
<div class="field">
|
||||
<input id={field.key} type="text" bind:value={field.value}
|
||||
on:input={() => field.changed = true}/>
|
||||
{#if field.changed}
|
||||
<button on:click={async () => { if (await callback()) field.changed = false } }
|
||||
transition:slide={{axis: 'x'}}>
|
||||
{ts('settings.profile.save')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.field
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
width: 100%
|
||||
|
||||
input
|
||||
flex: 1
|
||||
</style>
|
||||
19
AquaNet/src/libs/config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
export const AQUA_HOST = 'https://aquadx.net/aqua'
|
||||
export const DATA_HOST = 'https://aquadx.net'
|
||||
|
||||
// This will be displayed for users to connect from the client
|
||||
export const AQUA_CONNECTION = 'aquadx.hydev.org'
|
||||
|
||||
export const TURNSTILE_SITE_KEY = '0x4AAAAAAASGA2KQEIelo9P9'
|
||||
export const DISCORD_INVITE = 'https://discord.gg/FNgveqFF7s'
|
||||
export const TELEGRAM_INVITE = 'https://t.me/+zBL4RZdyfvUzZGU1'
|
||||
export const QQ_INVITE = 'https://qm.qq.com/q/wvNXbXbHbO'
|
||||
|
||||
// UI
|
||||
export const FADE_OUT = { duration: 200 }
|
||||
export const FADE_IN = { delay: 400 }
|
||||
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
|
||||
|
||||
// USERBOX_ASSETS
|
||||
export const HAS_USERBOX_ASSETS = true
|
||||
152
AquaNet/src/libs/generalTypes.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
export interface TrendEntry {
|
||||
date: string
|
||||
rating: number
|
||||
plays: number
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
luid: string
|
||||
registerTime: string
|
||||
accessTime: string
|
||||
linked: boolean
|
||||
isGhost: boolean
|
||||
}
|
||||
|
||||
export interface AquaNetUser {
|
||||
username: string
|
||||
email: string
|
||||
displayName: string
|
||||
country: string
|
||||
lastLogin: number
|
||||
regTime: number
|
||||
profileLocation: string
|
||||
profileBio: string
|
||||
profilePicture: string
|
||||
emailConfirmed: boolean
|
||||
ghostCard: Card
|
||||
cards: Card[]
|
||||
computedName: string,
|
||||
password: string,
|
||||
optOutOfLeaderboard: boolean,
|
||||
}
|
||||
|
||||
export interface CardSummaryGame {
|
||||
name: string
|
||||
rating: number
|
||||
lastLogin: string
|
||||
}
|
||||
|
||||
export interface CardSummary {
|
||||
mai2: CardSummaryGame | null
|
||||
chu3: CardSummaryGame | null
|
||||
ongeki: CardSummaryGame | null
|
||||
diva: CardSummaryGame | null
|
||||
wacca: CardSummaryGame | null
|
||||
}
|
||||
|
||||
|
||||
export interface ConfirmProps {
|
||||
title: string
|
||||
message: string
|
||||
confirm: () => void
|
||||
cancel?: () => void
|
||||
dangerous?: boolean
|
||||
}
|
||||
|
||||
export interface GenericGamePlaylog {
|
||||
musicId: number
|
||||
level: number
|
||||
playDate: string
|
||||
achievement: number
|
||||
maxCombo: number
|
||||
totalCombo: number
|
||||
afterRating: number
|
||||
beforeRating: number
|
||||
isFullCombo?: boolean
|
||||
isAllPerfect?: boolean
|
||||
isAllJustice?: boolean
|
||||
}
|
||||
|
||||
export interface GenericRanking {
|
||||
name: string
|
||||
username: string
|
||||
rank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
lastSeen: string
|
||||
}
|
||||
|
||||
export interface RankCount {
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface GenericGameSummary {
|
||||
name: string
|
||||
iconId: number
|
||||
aquaUser?: AquaNetUser
|
||||
serverRank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
ratingHighest: number
|
||||
ranks: RankCount[]
|
||||
detailedRanks: { [key: number]: { [key: string]: number } }
|
||||
maxCombo: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
totalScore: number
|
||||
plays: number
|
||||
totalPlayTime: number
|
||||
joined: string
|
||||
lastSeen: string
|
||||
lastVersion: string
|
||||
ratingComposition: { [key: string]: any }
|
||||
recent: GenericGamePlaylog[]
|
||||
rival?: boolean
|
||||
}
|
||||
|
||||
export interface MusicMeta {
|
||||
name: string,
|
||||
composer: string,
|
||||
bpm: number,
|
||||
ver: number,
|
||||
notes: {
|
||||
lv: number
|
||||
designer: string
|
||||
lv_id: number
|
||||
notes: number
|
||||
}[],
|
||||
worldsEndTag?: string
|
||||
worldsEndStars?: number
|
||||
}
|
||||
|
||||
export type AllMusic = { [key: string]: MusicMeta }
|
||||
|
||||
export interface GameOption {
|
||||
key: string
|
||||
value: any
|
||||
type: 'Boolean' | 'String'
|
||||
game: string
|
||||
|
||||
changed?: boolean
|
||||
}
|
||||
|
||||
export interface UserItem { itemKind: number, itemId: number, stock: number }
|
||||
export interface UserBox {
|
||||
userName: string,
|
||||
nameplateId: number,
|
||||
frameId: number,
|
||||
characterId: number,
|
||||
trophyId: number,
|
||||
mapIconId: number,
|
||||
voiceId: number,
|
||||
avatarWear: number,
|
||||
avatarHead: number,
|
||||
avatarFace: number,
|
||||
avatarSkin: number,
|
||||
avatarItem: number,
|
||||
avatarFront: number,
|
||||
avatarBack: number,
|
||||
}
|
||||
24
AquaNet/src/libs/hooks/useLocalStorage.svelte.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const useLocalStorage = <T>(key: string, initialValue: T) => {
|
||||
let value = initialValue;
|
||||
|
||||
const currentValue = localStorage.getItem(key);
|
||||
if (currentValue) value = JSON.parse(currentValue);
|
||||
|
||||
const save = () => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return value;
|
||||
},
|
||||
set value(v: T) {
|
||||
value = v;
|
||||
save();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default useLocalStorage;
|
||||
52
AquaNet/src/libs/i18n.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { EN_REF, type LocalizedMessages } from "./i18n/en_ref";
|
||||
import { ZH } from "./i18n/zh";
|
||||
import type { GameName } from "./scoring";
|
||||
|
||||
type Lang = 'en' | 'zh'
|
||||
|
||||
const msgs: Record<Lang, LocalizedMessages> = {
|
||||
en: EN_REF,
|
||||
zh: ZH
|
||||
}
|
||||
|
||||
|
||||
let lang: Lang = 'en'
|
||||
|
||||
// Infer language from browser
|
||||
if (navigator.language.startsWith('zh')) {
|
||||
lang = 'zh'
|
||||
}
|
||||
|
||||
export function ts(key: string, variables?: { [index: string]: any }) {
|
||||
return t(key as keyof LocalizedMessages, variables)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the translation for the given key
|
||||
*
|
||||
* TODO: Check for translation completion on build
|
||||
*
|
||||
* @param key
|
||||
* @param variables
|
||||
*/
|
||||
export function t(key: keyof LocalizedMessages, variables?: { [index: string]: any }) {
|
||||
// Check if the key exists
|
||||
let msg = msgs[lang][key]
|
||||
if (!msg) {
|
||||
// Check if the key exists in English
|
||||
if (!(msg = msgs.en[key])) {
|
||||
msg = key
|
||||
console.error(`ERROR!! Missing translation reference entry (English) for ${key}`)
|
||||
}
|
||||
else console.warn(`Missing translation for ${key} in ${lang}`)
|
||||
}
|
||||
// Replace variables
|
||||
if (variables) {
|
||||
return msg.replace(/\${(.*?)}/g, (_: string, v: string | number) => variables[v] + "")
|
||||
}
|
||||
return msg
|
||||
}
|
||||
Object.assign(window, { t })
|
||||
|
||||
export const GAME_TITLE: { [key in GameName]: string } =
|
||||
{chu3: t("game.chu3"), mai2: t("game.mai2"), ongeki: t("game.ongeki"), wacca: t("game.wacca")}
|
||||
187
AquaNet/src/libs/i18n/en_ref.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
export const EN_REF_USER = {
|
||||
'UserHome.ServerRank': 'Server Rank',
|
||||
'UserHome.DXRating': 'DX Rating',
|
||||
'UserHome.Rating': 'Rating',
|
||||
'UserHome.Statistics': 'Statistics',
|
||||
'UserHome.Accuracy': 'Accuracy',
|
||||
'UserHome.MaxCombo': 'Max Combo',
|
||||
'UserHome.FullCombo': 'Full Combo',
|
||||
'UserHome.AllPerfect': 'All Perfect',
|
||||
'UserHome.DXScore': 'DX Score',
|
||||
'UserHome.Score': 'Score',
|
||||
'UserHome.PlayActivity': ' Play Activity',
|
||||
'UserHome.Plays': 'Plays',
|
||||
'UserHome.PlayTime': 'Play Time',
|
||||
'UserHome.FirstSeen': 'First Seen',
|
||||
'UserHome.LastSeen': 'Last Seen',
|
||||
'UserHome.Version': 'Last Version',
|
||||
'UserHome.RecentScores': 'Recent Scores',
|
||||
'UserHome.NoData': 'No data in the past ${days} days',
|
||||
'UserHome.UnknownSong': '(unknown song)',
|
||||
'UserHome.Settings': 'Settings',
|
||||
'UserHome.NoValidGame': 'The user hasn\'t played any game yet.',
|
||||
'UserHome.ShowRanksDetails': 'Click to show details',
|
||||
'UserHome.RankDetail.Title': 'Achievement Details',
|
||||
'UserHome.RankDetail.Level': 'Level',
|
||||
'UserHome.B50': 'B50',
|
||||
'UserHome.AddRival': "Add to Rival",
|
||||
'UserHome.RemoveRival': "Remove from Rival",
|
||||
'UserHome.InvalidGame': "Game ${game} is not supported on the web UI yet. We only support maimai, chunithm, wacca, and ongeki for now.",
|
||||
}
|
||||
|
||||
export const EN_REF_Welcome = {
|
||||
'back': 'Back',
|
||||
'email': 'Email',
|
||||
'password': 'Password',
|
||||
'username': 'Username',
|
||||
'welcome.btn-login': 'Log in',
|
||||
'welcome.btn-signup': 'Sign up',
|
||||
'welcome.email-password-missing': 'Email and password are required',
|
||||
'welcome.username-missing': 'Username/email is required',
|
||||
'welcome.waiting-turnstile': 'Waiting for Turnstile to verify your network environment...',
|
||||
'welcome.turnstile-error': 'Error verifying your network environment. Please turn off your VPN and try again.',
|
||||
'welcome.turnstile-timeout': 'Network verification timed out. Please try again.',
|
||||
'welcome.verification-sent': 'A verification email has been sent to ${email}. Please check your inbox!',
|
||||
'welcome.verify-state-0': 'You haven\'t verified your email. A verification email had been sent to your inbox less than a minute ago. Please check your inbox!',
|
||||
'welcome.verify-state-1': 'You haven\'t verified your email. We\'ve already sent 3 emails over the last 24 hours so we\'ll not send another one. Please check your inbox!',
|
||||
'welcome.verify-state-2': 'You haven\'t verified your email. We just sent you another verification email. Please check your inbox!',
|
||||
'welcome.verifying': 'Verifying your email... please wait.',
|
||||
'welcome.verified': 'Your email has been verified! You can now log in now.',
|
||||
'welcome.verification-failed': 'Verification failed: ${message}. Please try again.',
|
||||
}
|
||||
|
||||
export const EN_REF_LEADERBOARD = {
|
||||
'Leaderboard.Title': 'Server Leaderboard',
|
||||
'Leaderboard.Rank': 'Rank',
|
||||
'Leaderboard.Rating': 'Rating',
|
||||
'Leaderboard.Accuracy': 'Accuracy',
|
||||
'Leaderboard.FC': 'FC',
|
||||
'Leaderboard.AP': 'AP',
|
||||
}
|
||||
|
||||
export const EN_REF_GENERAL = {
|
||||
'game.mai2': 'Mai',
|
||||
'game.chu3': 'Chuni',
|
||||
'game.ongeki': 'Ongeki',
|
||||
'game.wacca': 'Wacca',
|
||||
'status.error': 'Error',
|
||||
'status.error.hint': 'Something went wrong, please try again later or ',
|
||||
'status.error.hint.link': 'join our discord for support.',
|
||||
'status.detail': 'Detail: ${detail}',
|
||||
'action.refresh': 'Refresh',
|
||||
'action.cancel': 'Cancel',
|
||||
'action.confirm': 'Confirm',
|
||||
}
|
||||
|
||||
export const EN_REF_HOME = {
|
||||
'home.nav.portal': 'Portal',
|
||||
'home.nav.link-card': 'Link Card',
|
||||
'home.nav.game-setup': 'Game Setup',
|
||||
'home.manage-cards': 'Manage Cards',
|
||||
'home.manage-cards-description': 'Link, unlink, and manage your cards.',
|
||||
'home.link-card': 'Link Card',
|
||||
'home.link-cards-description': 'Link your Amusement IC / Aime card to play games.',
|
||||
'home.join-community': 'Join Community',
|
||||
'home.join-community-description': 'Join our community to chat with other players and get help.',
|
||||
'home.setup': 'Setup Connection',
|
||||
'home.setup-description': 'If you own a cab or arcade setup, begin setting up the connection.',
|
||||
'home.import': 'Import Player Data',
|
||||
'home.import-description': 'If you are from another server, you can import your data here.',
|
||||
'home.linkcard.cards': 'Your Cards',
|
||||
'home.linkcard.description': 'Here are the cards you have linked to your account',
|
||||
'home.linkcard.account-card': 'Account Card',
|
||||
'home.linkcard.registered': 'Registered',
|
||||
'home.linkcard.lastused': 'Last used',
|
||||
'home.linkcard.enter-info': 'Please enter the following information',
|
||||
'home.linkcard.access-code': 'The 20-digit access code on the back of your card. (If it doesn\'t work, please try scanning your card in game and enter the access code shown on screen)',
|
||||
'home.linkcard.enter-sn1': 'Download the NFC Tools app on your phone',
|
||||
'home.linkcard.enter-sn2': 'and scan your card. Then, enter the Serial Number.',
|
||||
'home.linkcard.link': 'Link',
|
||||
'home.linkcard.data-conflict': 'Data Conflict',
|
||||
'home.linkcard.name': 'Name',
|
||||
'home.linkcard.rating': 'Rating',
|
||||
'home.linkcard.last-login': 'Last Login',
|
||||
'home.linkcard.linked-own': 'This card is already linked to your account',
|
||||
'home.linkcard.linked-another': 'This card is already linked to another account',
|
||||
'home.linkcard.notfound': 'Card not found',
|
||||
'home.linkcard.unlink': 'Unlink Card',
|
||||
'home.linkcard.unlink-notice': 'Are you sure you want to unlink this card?',
|
||||
'home.setup.welcome': 'Welcome! If you own an arcade cabinet or game setup, please follow the instructions below to set up the connection with AquaDX.',
|
||||
'home.setup.blockquote': 'We assume that you already have the required files and can run the game (e.g. ROM and segatools) that come with the cabinet or game setup. If not, please contact the seller of your device for the required files, as we will not provide them for copyright reasons.',
|
||||
'home.setup.get': 'Get started',
|
||||
'home.setup.edit': 'Please edit your segatools.ini file and modify the following lines',
|
||||
'home.setup.test': 'Then, after you restart the game, you should be able to connect to AquaDX. Please verify that the network tests are all GOOD in the test menu.',
|
||||
'home.setup.ask': 'If you have any questions, please ask in our',
|
||||
'home.setup.support': 'server',
|
||||
'home.setup.keychip-tips': 'This is your unique keychip, do not share it with anyone',
|
||||
'home.import.unknown-game': 'Unknown game type. Currently only maimai and chunithm are supported for importing.',
|
||||
'home.import.new-data': 'Data to import',
|
||||
'home.import.data-conflict': 'Proceed will override your current data',
|
||||
}
|
||||
|
||||
export const EN_REF_SETTINGS = {
|
||||
'settings.title': 'Settings',
|
||||
'settings.tabs.profile': 'Profile',
|
||||
'settings.tabs.game': 'Game',
|
||||
'settings.tabs.chu3': 'Chuni',
|
||||
'settings.tabs.mai2': 'Mai',
|
||||
'settings.tabs.wacca': 'Wacca',
|
||||
'settings.fields.unlockMusic.name': 'Unlock All Music',
|
||||
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.',
|
||||
'settings.fields.unlockChara.name': 'Unlock All Characters',
|
||||
'settings.fields.unlockChara.desc': 'Unlock all characters, voices, and partners in game.',
|
||||
'settings.fields.unlockCollectables.name': 'Unlock All Collectables',
|
||||
'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.',
|
||||
'settings.fields.unlockTickets.name': 'Unlock All Tickets',
|
||||
'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (note: maimai still limits which tickets can be used).',
|
||||
'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP',
|
||||
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
|
||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',
|
||||
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01',
|
||||
'settings.fields.chusanTeamName.name': 'Chuni: Team Name',
|
||||
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
|
||||
'settings.fields.chusanInfinitePenguins.name': 'Chuni: Infinite Penguins',
|
||||
'settings.fields.chusanInfinitePenguins.desc': 'Set penguin statues for character level prompting to 999.',
|
||||
'settings.fields.rounding.name': 'Score Rounding',
|
||||
'settings.fields.rounding.desc': 'Round the score to one decimal place',
|
||||
'settings.fields.optOutOfLeaderboard.name': 'Opt Out of Leaderboard',
|
||||
'settings.fields.optOutOfLeaderboard.desc': 'You will still be able to see yourself on the leaderboard after logging in',
|
||||
'settings.mai2.name': 'Player Name',
|
||||
'settings.profile.picture': 'Profile Picture',
|
||||
'settings.profile.upload-new': 'Upload New',
|
||||
'settings.profile.save': 'Save',
|
||||
'settings.profile.name': 'Display Name',
|
||||
'settings.profile.username': 'Username',
|
||||
'settings.profile.password': 'Password',
|
||||
'settings.profile.location': 'Location',
|
||||
'settings.profile.bio': 'Bio',
|
||||
'settings.profile.unset': 'Unset',
|
||||
'settings.profile.unchanged': 'Unchanged',
|
||||
'settings.export': 'Export Player Data',
|
||||
}
|
||||
|
||||
export const EN_REF_USERBOX = {
|
||||
'userbox.header.general': 'General Settings',
|
||||
'userbox.header.userbox': 'UserBox Settings',
|
||||
'userbox.header.preview': 'UserBox Preview',
|
||||
'userbox.nameplateId': 'Nameplate',
|
||||
'userbox.frameId': 'Frame',
|
||||
'userbox.trophyId': 'Trophy (Title)',
|
||||
'userbox.mapIconId': 'Map Icon',
|
||||
'userbox.voiceId': 'System Voice',
|
||||
'userbox.avatarWear': 'Avatar Wear',
|
||||
'userbox.avatarHead': 'Avatar Head',
|
||||
'userbox.avatarFace': 'Avatar Face',
|
||||
'userbox.avatarSkin': 'Avatar Skin',
|
||||
'userbox.avatarItem': 'Avatar Item',
|
||||
'userbox.avatarFront': 'Avatar Front',
|
||||
'userbox.avatarBack': 'Avatar Back',
|
||||
'userbox.preview.notice': 'To honor the copyright, we cannot host the images of the userbox items. However, if someone else is willing to provide the images, you can enter their URL here and it will be displayed.',
|
||||
'userbox.preview.url': 'Image URL',
|
||||
'userbox.error.nodata': 'Chuni data not found',
|
||||
}
|
||||
|
||||
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
||||
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX }
|
||||
|
||||
export type LocalizedMessages = typeof EN_REF
|
||||
195
AquaNet/src/libs/i18n/zh.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
EN_REF_GENERAL,
|
||||
EN_REF_HOME,
|
||||
EN_REF_LEADERBOARD,
|
||||
EN_REF_SETTINGS,
|
||||
EN_REF_USER,
|
||||
EN_REF_USERBOX,
|
||||
type EN_REF_Welcome
|
||||
} from "./en_ref";
|
||||
|
||||
const zhUser: typeof EN_REF_USER = {
|
||||
'UserHome.ServerRank': '服务器排名',
|
||||
'UserHome.DXRating': 'DX B50',
|
||||
'UserHome.Rating': '评分',
|
||||
'UserHome.Statistics': '统计数据',
|
||||
'UserHome.Accuracy': '准确率',
|
||||
'UserHome.MaxCombo': '最大连击',
|
||||
'UserHome.FullCombo': '全连曲目',
|
||||
'UserHome.AllPerfect': '完美曲目',
|
||||
'UserHome.DXScore': 'DX 总分',
|
||||
'UserHome.Score': '总分',
|
||||
'UserHome.PlayActivity': '游戏活动',
|
||||
'UserHome.Plays': '出勤次数',
|
||||
'UserHome.PlayTime': '游玩时间',
|
||||
'UserHome.FirstSeen': '发现新大陆',
|
||||
'UserHome.LastSeen': '上次出勤',
|
||||
'UserHome.Version': '游戏版本',
|
||||
'UserHome.RecentScores': '成绩',
|
||||
'UserHome.NoData': '过去 ${days} 天内没有玩过',
|
||||
'UserHome.UnknownSong': "(未知曲目)",
|
||||
'UserHome.Settings': '设置',
|
||||
'UserHome.NoValidGame': "用户还没有玩过游戏",
|
||||
'UserHome.ShowRanksDetails': "点击显示评分详细",
|
||||
'UserHome.RankDetail.Title': '评分详细',
|
||||
'UserHome.RankDetail.Level': "等级",
|
||||
'UserHome.B50': "B50",
|
||||
'UserHome.AddRival': "添加劲敌",
|
||||
'UserHome.RemoveRival': "移除劲敌",
|
||||
'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、Wacca 和音击。",
|
||||
}
|
||||
|
||||
const zhWelcome: typeof EN_REF_Welcome = {
|
||||
'back': '返回',
|
||||
'email': '邮箱',
|
||||
'password': '密码',
|
||||
'username': '用户名',
|
||||
'welcome.btn-login': '登录',
|
||||
'welcome.btn-signup': '注册',
|
||||
'welcome.email-password-missing': '邮箱和密码必须填哦',
|
||||
'welcome.username-missing': '用户名/邮箱必须填哦',
|
||||
'welcome.waiting-turnstile': '正在验证网络环境...',
|
||||
'welcome.turnstile-error': '验证网络环境出错了,请关闭VPN后重试',
|
||||
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
|
||||
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
|
||||
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的24小时内已经发送了3封验证邮件,所以我们不会再发送了,请翻翻收件箱',
|
||||
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
|
||||
'welcome.verifying': '正在验证邮箱...请稍等',
|
||||
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
|
||||
'welcome.verification-failed': '验证失败:${message}。请重试',
|
||||
}
|
||||
|
||||
const zhLeaderboard: typeof EN_REF_LEADERBOARD = {
|
||||
'Leaderboard.Title': '排行榜',
|
||||
'Leaderboard.Rank': '排名',
|
||||
'Leaderboard.Rating': '评分',
|
||||
'Leaderboard.Accuracy': '准确率',
|
||||
'Leaderboard.FC': 'FC',
|
||||
'Leaderboard.AP': 'AP',
|
||||
}
|
||||
|
||||
const zhGeneral: typeof EN_REF_GENERAL = {
|
||||
'game.mai2': "舞萌",
|
||||
'game.chu3': "中二",
|
||||
'game.ongeki': "音击",
|
||||
'game.wacca': "Wacca",
|
||||
"status.error": "发生错误",
|
||||
"status.error.hint": "出了一些问题,请稍后刷新重试或者",
|
||||
"status.error.hint.link": "加我们的 Discord 群问一问",
|
||||
"status.detail": "详细信息:${detail}",
|
||||
"action.refresh": "刷新",
|
||||
"action.cancel": "取消",
|
||||
"action.confirm": "确认",
|
||||
}
|
||||
|
||||
const zhHome: typeof EN_REF_HOME = {
|
||||
'home.nav.portal': "主页",
|
||||
'home.nav.link-card': "绑卡",
|
||||
'home.nav.game-setup': "连接设置",
|
||||
'home.manage-cards': '管理游戏卡',
|
||||
'home.manage-cards-description': '绑定、解绑、管理游戏数据卡',
|
||||
'home.link-card': '绑定游戏卡',
|
||||
'home.link-cards-description':'绑定游戏数据卡 (Amusement IC 或 Aime 卡) 后才可以访问游戏存档哦',
|
||||
'home.join-community': '加入群组',
|
||||
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
|
||||
'home.setup': '连接到 AquaDX',
|
||||
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
|
||||
'home.import': '导入玩家数据',
|
||||
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
|
||||
'home.linkcard.cards': "已绑卡片",
|
||||
'home.linkcard.description': "这些是您绑定到帐户的卡",
|
||||
'home.linkcard.account-card': "账户卡",
|
||||
'home.linkcard.registered': "注册于",
|
||||
'home.linkcard.lastused': "上次使用",
|
||||
'home.linkcard.enter-info': "请输入以下信息",
|
||||
'home.linkcard.access-code': "卡背面的20位卡号 (如果没有, 请尝试在游戏中扫描您的卡, 并输入屏幕上显示的卡号)",
|
||||
'home.linkcard.enter-sn1': "在您的手机",
|
||||
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
|
||||
'home.linkcard.link': "绑定",
|
||||
'home.linkcard.data-conflict': "卡号冲突",
|
||||
'home.linkcard.name': "名称",
|
||||
'home.linkcard.rating': "Rating",
|
||||
'home.linkcard.last-login': "上次登录",
|
||||
'home.linkcard.linked-own': "此卡已链接到您的帐户",
|
||||
'home.linkcard.linked-another': "此卡已链接到其他用户",
|
||||
'home.linkcard.notfound': "找不到卡",
|
||||
'home.linkcard.unlink': "取消链接",
|
||||
'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?",
|
||||
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台, 请按照以下说明设置以连接到 AquaDX.",
|
||||
'home.setup.blockquote': "我们假设您已经拥有所需的文件, 并且可以启动机台或手台附带的游戏 (例如 ROM 和 segatools )。如果没有, 请联系您设备的卖家以获取所需的文件, 因为出于版权原因, 我们不会提供这些文件。",
|
||||
'home.setup.get': "开始",
|
||||
'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行",
|
||||
'home.setup.test': "在您重新启动游戏后, 应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
|
||||
'home.setup.ask': "如果您有任何问题, 请加入我们的",
|
||||
'home.setup.support': "以获取支持",
|
||||
'home.setup.keychip-tips': "这是你的狗号, 不要与任何人分享",
|
||||
'home.import.unknown-game': '未知游戏类型 (目前导入只支持舞萌和中二)',
|
||||
'home.import.new-data': '要导入的数据',
|
||||
'home.import.data-conflict': '继续导入将覆盖现有数据',
|
||||
}
|
||||
|
||||
const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.title': '用户设置',
|
||||
'settings.tabs.profile': '个人资料',
|
||||
'settings.tabs.game': '游戏设置',
|
||||
'settings.tabs.chu3': '中二',
|
||||
'settings.tabs.mai2': '舞萌',
|
||||
'settings.tabs.wacca': 'Wacca',
|
||||
'settings.fields.unlockMusic.name': '解锁谱面',
|
||||
'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。',
|
||||
'settings.fields.unlockChara.name': '解锁角色',
|
||||
'settings.fields.unlockChara.desc': '在游戏中解锁所有角色、语音和伙伴。',
|
||||
'settings.fields.unlockCollectables.name': '解锁收藏品',
|
||||
'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。',
|
||||
'settings.fields.unlockTickets.name': '解锁游戏券',
|
||||
'settings.fields.unlockTickets.desc': '无限跑图券/解锁券(注:maimai 客户端仍限制一些券不能使用)。',
|
||||
'settings.fields.waccaInfiniteWp.name': 'Wacca: 无限 WP',
|
||||
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
|
||||
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员',
|
||||
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
|
||||
'settings.fields.chusanTeamName.name': '中二: 队伍名称',
|
||||
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
|
||||
'settings.fields.chusanInfinitePenguins.name': '中二: 无限企鹅',
|
||||
'settings.fields.chusanInfinitePenguins.desc': '将角色界限突破的企鹅雕像数量设置为 999。',
|
||||
'settings.fields.rounding.name': '分数舍入',
|
||||
'settings.fields.rounding.desc': '把分数四舍五入到一位小数',
|
||||
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
|
||||
'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己',
|
||||
'settings.mai2.name': '玩家名字',
|
||||
'settings.profile.picture': '头像',
|
||||
'settings.profile.upload-new': '上传',
|
||||
'settings.profile.save': '保存',
|
||||
'settings.profile.name': '昵称',
|
||||
'settings.profile.username': '用户名',
|
||||
'settings.profile.password': '密码',
|
||||
'settings.profile.location': '位置',
|
||||
'settings.profile.bio': '简介',
|
||||
'settings.profile.unset': '未设置',
|
||||
'settings.profile.unchanged': '未更改',
|
||||
'settings.export': '导出玩家数据',
|
||||
}
|
||||
|
||||
export const zhUserbox: typeof EN_REF_USERBOX = {
|
||||
'userbox.header.general': '游戏设置',
|
||||
'userbox.header.userbox': 'UserBox 设置',
|
||||
'userbox.header.preview': 'UserBox 预览',
|
||||
'userbox.nameplateId': '名牌',
|
||||
'userbox.frameId': '边框',
|
||||
'userbox.trophyId': '称号',
|
||||
'userbox.mapIconId': '地图图标',
|
||||
'userbox.voiceId': '系统语音',
|
||||
'userbox.avatarWear': '企鹅服饰',
|
||||
'userbox.avatarHead': '企鹅头饰',
|
||||
'userbox.avatarFace': '企鹅面部',
|
||||
'userbox.avatarSkin': '企鹅皮肤',
|
||||
'userbox.avatarItem': '企鹅物品',
|
||||
'userbox.avatarFront': '企鹅前景',
|
||||
'userbox.avatarBack': '企鹅背景',
|
||||
'userbox.preview.notice': '「生存战略」:为了尊重版权,我们不会提供游戏内物品的图片。但是如果你认识其他愿意提供图床的人,在这里输入 URL 就可以显示出预览。',
|
||||
'userbox.preview.url': '图床 URL',
|
||||
'userbox.error.nodata': '未找到中二数据',
|
||||
};
|
||||
|
||||
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
|
||||
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox }
|
||||
13
AquaNet/src/libs/maimai.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AQUA_HOST, DATA_HOST } from './config'
|
||||
|
||||
|
||||
export async function getMaimai(endpoint: string, params: any) {
|
||||
return await fetch(`${AQUA_HOST}/Maimai2Servlet/${endpoint}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params)
|
||||
}).then(res => res.json())
|
||||
}
|
||||
|
||||
export async function getMaimaiAllMusic(): Promise<{ [key: string]: any }> {
|
||||
return fetch(`${DATA_HOST}/maimai/meta/00/all-music.json`).then(it => it.json())
|
||||
}
|
||||
107
AquaNet/src/libs/maimaiTypes.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { MusicMeta } from "./generalTypes";
|
||||
|
||||
export interface Rating {
|
||||
musicId: number
|
||||
level: number
|
||||
achievement: number
|
||||
}
|
||||
|
||||
export interface ParsedRating extends Rating {
|
||||
music: MusicMeta,
|
||||
calc: number,
|
||||
rank: string
|
||||
}
|
||||
|
||||
export interface MaimaiUserSummaryEntry {
|
||||
name: string
|
||||
iconId: number
|
||||
serverRank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
ratingHighest: number
|
||||
ranks: { name: string, count: number }[]
|
||||
maxCombo: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
totalDxScore: number
|
||||
plays: number
|
||||
totalPlayTime: number
|
||||
joined: string
|
||||
lastSeen: string
|
||||
lastVersion: string
|
||||
best35: string
|
||||
best15: string
|
||||
recent: MaimaiUserPlaylog[]
|
||||
}
|
||||
|
||||
export interface MaimaiUserPlaylog {
|
||||
id: number;
|
||||
musicId: number;
|
||||
level: number;
|
||||
userPlayDate: string;
|
||||
trackNo: number;
|
||||
vsRank: number;
|
||||
achievement: number;
|
||||
deluxscore: number;
|
||||
scoreRank: number;
|
||||
maxCombo: number;
|
||||
totalCombo: number;
|
||||
maxSync: number;
|
||||
totalSync: number;
|
||||
tapCriticalPerfect: number;
|
||||
tapPerfect: number;
|
||||
tapGreat: number;
|
||||
tapGood: number;
|
||||
tapMiss: number;
|
||||
holdCriticalPerfect: number;
|
||||
holdPerfect: number;
|
||||
holdGreat: number;
|
||||
holdGood: number;
|
||||
holdMiss: number;
|
||||
slideCriticalPerfect: number;
|
||||
slidePerfect: number;
|
||||
slideGreat: number;
|
||||
slideGood: number;
|
||||
slideMiss: number;
|
||||
touchCriticalPerfect: number;
|
||||
touchPerfect: number;
|
||||
touchGreat: number;
|
||||
touchGood: number;
|
||||
touchMiss: number;
|
||||
breakCriticalPerfect: number;
|
||||
breakPerfect: number;
|
||||
breakGreat: number;
|
||||
breakGood: number;
|
||||
breakMiss: number;
|
||||
isTap: boolean;
|
||||
isHold: boolean;
|
||||
isSlide: boolean;
|
||||
isTouch: boolean;
|
||||
isBreak: boolean;
|
||||
isCriticalDisp: boolean;
|
||||
isFastLateDisp: boolean;
|
||||
fastCount: number;
|
||||
lateCount: number;
|
||||
isAchieveNewRecord: boolean;
|
||||
isDeluxscoreNewRecord: boolean;
|
||||
comboStatus: number;
|
||||
syncStatus: number;
|
||||
isClear: boolean;
|
||||
beforeRating: number;
|
||||
afterRating: number;
|
||||
beforeGrade: number;
|
||||
afterGrade: number;
|
||||
afterGradeRank: number;
|
||||
beforeDeluxRating: number;
|
||||
afterDeluxRating: number;
|
||||
isPlayTutorial: boolean;
|
||||
isEventMode: boolean;
|
||||
isFreedomMode: boolean;
|
||||
playMode: number;
|
||||
isNewFree: boolean;
|
||||
trialPlayAchievement: number;
|
||||
extNum1: number;
|
||||
extNum2: number;
|
||||
extNum4: number;
|
||||
extBool1: boolean;
|
||||
}
|
||||
24
AquaNet/src/libs/ongekiTypes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { GenericGamePlaylog } from "./generalTypes";
|
||||
|
||||
export interface OngekiUserSummaryEntry {
|
||||
name: string
|
||||
iconId: number
|
||||
serverRank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
ratingHighest: number
|
||||
ranks: { name: string, count: number }[]
|
||||
maxCombo: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
totalDxScore: number
|
||||
plays: number
|
||||
totalPlayTime: number
|
||||
joined: string
|
||||
lastSeen: string
|
||||
lastVersion: string
|
||||
best30: string
|
||||
best15: string
|
||||
recent10: string
|
||||
recent: GenericGamePlaylog[]
|
||||
}
|
||||
153
AquaNet/src/libs/scoring.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { DATA_HOST } from "./config"
|
||||
import type { MusicMeta } from "./generalTypes"
|
||||
|
||||
export type GameName = 'mai2' | 'chu3' | 'ongeki' | 'wacca'
|
||||
|
||||
const multTable = {
|
||||
'mai2': [
|
||||
[ 100.5, 22.4, 'SSSp' ],
|
||||
[ 100.0, 21.6, 'SSS' ],
|
||||
[ 99.5, 21.1, 'SSp' ],
|
||||
[ 99, 20.8, 'SS' ],
|
||||
[ 98, 20.3, 'Sp' ],
|
||||
[ 97, 20, 'S' ],
|
||||
[ 94, 16.8, 'AAA' ],
|
||||
[ 90, 15.2, 'AA' ],
|
||||
[ 80, 13.6, 'A' ],
|
||||
[ 75, 12, 'BBB' ],
|
||||
[ 70, 11.2, 'BB' ],
|
||||
[ 60, 9.6, 'B' ],
|
||||
[ 50, 8, 'C' ],
|
||||
[ 40, 6.4, 'D' ],
|
||||
[ 30, 4.8, 'D' ],
|
||||
[ 20, 3.2, 'D' ],
|
||||
[ 10, 1.6, 'D' ],
|
||||
[ 0, 0, 'D' ]
|
||||
],
|
||||
|
||||
// TODO: Fill in multipliers for Chunithm and Ongeki
|
||||
'chu3': [
|
||||
[ 100.9, 215, 'SSS+' ],
|
||||
[ 100.75, 200, 'SSS' ],
|
||||
[ 100.0, 0, 'SS' ],
|
||||
[ 97.5, 0, 'S' ],
|
||||
[ 95.0, 0, 'AAA' ],
|
||||
[ 92.5, 0, 'AA' ],
|
||||
[ 90.0, 0, 'A' ],
|
||||
[ 80.0, 0, 'BBB' ],
|
||||
[ 70.0, 0, 'BB' ],
|
||||
[ 60.0, 0, 'B' ],
|
||||
[ 50.0, 0, 'C' ],
|
||||
[ 0.0, 0, 'D' ]
|
||||
],
|
||||
|
||||
'ongeki': [
|
||||
[ 100.75, 0, 'SSS+' ],
|
||||
[ 100.0, 0, 'SSS' ],
|
||||
[ 99.0, 0, 'SS' ],
|
||||
[ 97.0, 0, 'S' ],
|
||||
[ 94.0, 0, 'AAA' ],
|
||||
[ 90.0, 0, 'AA' ],
|
||||
[ 85.0, 0, 'A' ],
|
||||
[ 80.0, 0, 'BBB' ],
|
||||
[ 75.0, 0, 'BB' ],
|
||||
[ 70.0, 0, 'B' ],
|
||||
[ 50.0, 0, 'C' ],
|
||||
[ 0.0, 0, 'D' ]
|
||||
],
|
||||
|
||||
'wacca': [
|
||||
[ 100.0, 0, 'AP' ],
|
||||
[ 98.0, 0, 'SSS' ],
|
||||
[ 95.0, 0, 'SS' ],
|
||||
[ 90.0, 0, 'S' ],
|
||||
[ 85.0, 0, 'AAA' ],
|
||||
[ 80.0, 0, 'AA' ],
|
||||
[ 70.0, 0, 'A' ],
|
||||
[ 60.0, 0, 'B' ],
|
||||
[ 1.0, 0, 'C' ],
|
||||
[ 0.0, 0, 'D' ]
|
||||
]
|
||||
}
|
||||
|
||||
export function getMult(achievement: number, game: GameName) {
|
||||
achievement /= 10000
|
||||
const mt = multTable[game]
|
||||
for (let i = 0; i < mt.length; i++) {
|
||||
if (achievement >= (mt[i][0] as number)) return mt[i]
|
||||
}
|
||||
return [ 0, 0, 0 ]
|
||||
}
|
||||
|
||||
export function roundFloor(achievement: number, game: GameName, digits = 2) {
|
||||
// Round, but if the rounded number reaches the next rank, use floor instead
|
||||
const mult = getMult(achievement, game);
|
||||
achievement /= 10000
|
||||
const rounded = achievement.toFixed(digits);
|
||||
if (getMult(+rounded * 10000, game)[2] === mult[2] && rounded !== '101.0') return rounded;
|
||||
return (+rounded - Math.pow(10, -digits)).toFixed(digits);
|
||||
}
|
||||
|
||||
export function chusanRating(lv: number, score: number) {
|
||||
lv = lv * 100
|
||||
if (score >= 1009000) return lv + 215; // SSS+
|
||||
if (score >= 1007500) return lv + 200 + (score - 1007500) / 100; // SSS
|
||||
if (score >= 1005000) return lv + 150 + (score - 1005000) / 50; // SS+
|
||||
if (score >= 1000000) return lv + 100 + (score - 1000000) / 100; // SS
|
||||
if (score >= 975000) return lv + (score - 975000) / 250; // S+, S
|
||||
if (score >= 925000) return lv - 300 + (score - 925000) * 3 / 500; // AA
|
||||
if (score >= 900000) return lv - 500 + (score - 900000) * 4 / 500; // A
|
||||
if (score >= 800000) return ((lv - 500) / 2 + (score - 800000) * ((lv - 500) / 2) / (100000)); // BBB
|
||||
return 0; // C
|
||||
}
|
||||
|
||||
export interface ParsedComposition {
|
||||
name?: string
|
||||
musicId: number
|
||||
diffId: number // ID of the difficulty
|
||||
score: number
|
||||
cutoff: number
|
||||
mult: number
|
||||
rank: string // e.g. 'SSS+'
|
||||
difficulty?: number // Actual difficulty of the map
|
||||
img: string
|
||||
ratingChange?: string // Rating change after playing this map
|
||||
}
|
||||
|
||||
|
||||
export function parseComposition(item: string, allMusics: Record<string, MusicMeta>, game: GameName): ParsedComposition {
|
||||
// Chuni & ongeki: musicId, difficultId, score
|
||||
// Mai: musicId, level (difficultyId), romVersion, achievement (score)
|
||||
const mapData = item.split(':').map(Number)
|
||||
if (game === 'mai2') mapData.splice(2, 1)
|
||||
const [ musicId, diffId, score ] = mapData
|
||||
const meta = allMusics[musicId]
|
||||
|
||||
// Get score multiplier
|
||||
const tup = getMult(score, game)
|
||||
const [ cutoff, mult ] = [ +tup[0], +tup[1] ]
|
||||
const rank = "" + tup[2]
|
||||
|
||||
let diff = meta?.notes?.[diffId === 10 ? 0 : diffId]?.lv
|
||||
|
||||
function calcDxChange() {
|
||||
if (!diff) return
|
||||
if (game === 'mai2')
|
||||
return Math.floor(diff * mult * (Math.min(100.5, score / 10000) / 100)).toFixed(0)
|
||||
if (game === 'chu3')
|
||||
return (chusanRating(diff, score) / 100).toFixed(1)
|
||||
}
|
||||
|
||||
return {
|
||||
name: meta?.name,
|
||||
musicId,
|
||||
diffId,
|
||||
score,
|
||||
cutoff,
|
||||
mult,
|
||||
rank,
|
||||
difficulty: diff,
|
||||
img: `${DATA_HOST}/d/${game}/music/00${mapData[0].toString().padStart(6, '0').substring(2)}.png`,
|
||||
ratingChange: calcDxChange()
|
||||
}
|
||||
}
|
||||
313
AquaNet/src/libs/sdk.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { AQUA_HOST, DATA_HOST } from './config'
|
||||
import type {
|
||||
AllMusic,
|
||||
Card,
|
||||
CardSummary,
|
||||
GenericGameSummary,
|
||||
GenericRanking,
|
||||
TrendEntry,
|
||||
AquaNetUser, GameOption,
|
||||
UserBox,
|
||||
UserItem
|
||||
} from './generalTypes'
|
||||
import type { GameName } from './scoring'
|
||||
|
||||
interface RequestInitWithParams extends RequestInit {
|
||||
params?: { [index: string]: string }
|
||||
localCache?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a fetch url
|
||||
*
|
||||
* @param input Fetch url input
|
||||
* @param callback Callback for modification
|
||||
*/
|
||||
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
|
||||
let u = new URL((input instanceof Request) ? input.url : input)
|
||||
const result = callback(u)
|
||||
if (result) u = result
|
||||
if (input instanceof Request) {
|
||||
// @ts-ignore
|
||||
return { url: u, ...input }
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with url parameters
|
||||
*/
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
|
||||
return fetch(reconstructUrl(input, u => {
|
||||
u.search = new URLSearchParams(init?.params ?? {}).toString()
|
||||
}), init)
|
||||
}
|
||||
|
||||
const cache: { [index: string]: any } = {}
|
||||
|
||||
export async function post(endpoint: string, params: Record<string, any> = {}, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function get(endpoint: string, params:any,init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'GET',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function put(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
...init?.headers
|
||||
},
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function realPost(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
...init?.headers
|
||||
},
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* aqua.net.UserRegistrar
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
|
||||
return await post('/api/v2/user/register', user)
|
||||
}
|
||||
async function login(user: { email: string, password: string, turnstile: string }) {
|
||||
const data = await post('/api/v2/user/login', user)
|
||||
|
||||
// Put token into local storage
|
||||
localStorage.setItem('token', data.token)
|
||||
}
|
||||
|
||||
const isLoggedIn = () => !!localStorage.getItem('token')
|
||||
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
|
||||
|
||||
export const USER = {
|
||||
register,
|
||||
login,
|
||||
confirmEmail: (token: string) =>
|
||||
post('/api/v2/user/confirm-email', { token }),
|
||||
me: (): Promise<AquaNetUser> => {
|
||||
ensureLoggedIn()
|
||||
return post('/api/v2/user/me', {})
|
||||
},
|
||||
keychip: (): Promise<string> =>
|
||||
post('/api/v2/user/keychip', {}).then(it => it.keychip),
|
||||
setting: (key: string, value: string) =>
|
||||
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
|
||||
uploadPfp: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return post('/api/v2/user/upload-pfp', { }, { method: 'POST', body: formData })
|
||||
},
|
||||
isLoggedIn,
|
||||
ensureLoggedIn,
|
||||
}
|
||||
|
||||
export const USERBOX = {
|
||||
getProfile: (): Promise<{ user: UserBox, items: UserItem[] }> =>
|
||||
get('/api/v2/game/chu3/user-box', {}),
|
||||
setUserBox: (d: { field: string, value: number | string }) =>
|
||||
post(`/api/v2/game/chu3/user-detail-set`, d),
|
||||
}
|
||||
|
||||
export const CARD = {
|
||||
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
|
||||
post('/api/v2/card/summary', { cardId }),
|
||||
link: (props: { cardId: string, migrate: string }) =>
|
||||
post('/api/v2/card/link', props),
|
||||
unlink: (cardId: string) =>
|
||||
post('/api/v2/card/unlink', { cardId }),
|
||||
userGames: (username: string): Promise<CardSummary> =>
|
||||
post('/api/v2/card/user-games', { username }),
|
||||
}
|
||||
|
||||
export const GAME = {
|
||||
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
|
||||
post(`/api/v2/game/${game}/trend`, { username }),
|
||||
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
|
||||
post(`/api/v2/game/${game}/user-summary`, { username }),
|
||||
ranking: (game: GameName): Promise<GenericRanking[]> =>
|
||||
post(`/api/v2/game/${game}/ranking`, { }),
|
||||
changeName: (game: GameName, newName: string): Promise<{ newName: string }> =>
|
||||
post(`/api/v2/game/${game}/change-name`, { newName }),
|
||||
export: (game: GameName): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/export`),
|
||||
import: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/import`, {}, { body: JSON.stringify(data) }),
|
||||
importMusicDetail: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/import-music-detail`, {}, {body: JSON.stringify(data), headers: {'Content-Type': 'application/json'}}),
|
||||
setRival: (game: GameName, rivalUserName: string, isAdd: boolean) =>
|
||||
post(`/api/v2/game/${game}/set-rival`, { rivalUserName, isAdd }),
|
||||
}
|
||||
|
||||
export const DATA = {
|
||||
allMusic: (game: GameName): Promise<AllMusic> =>
|
||||
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json()),
|
||||
allItems: (game: GameName): Promise<Record<string, Record<string, any>>> =>
|
||||
fetch(`${DATA_HOST}/d/${game}/00/all-items.json`).then(it => it.json()),
|
||||
}
|
||||
|
||||
export const SETTING = {
|
||||
get: (): Promise<GameOption[]> =>
|
||||
post('/api/v2/settings/get', {}),
|
||||
set: (key: string, value: any) =>
|
||||
post('/api/v2/settings/set', { key, value: `${value}` }),
|
||||
}
|
||||
213
AquaNet/src/libs/ui.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
type ChartOptions,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from 'chart.js'
|
||||
import moment from 'moment/moment'
|
||||
// @ts-expect-error Cal-heatmap does not have proper types
|
||||
import CalHeatmap from 'cal-heatmap'
|
||||
// @ts-expect-error Cal-heatmap does not have proper types
|
||||
import CalTooltip from 'cal-heatmap/plugins/Tooltip'
|
||||
import { AQUA_HOST, DEFAULT_PFP } from "./config"
|
||||
import type { AquaNetUser } from "./generalTypes"
|
||||
|
||||
export function title(t: string) {
|
||||
document.title = `AquaNet - ${t}`
|
||||
}
|
||||
|
||||
export function registerChart() {
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
TimeScale
|
||||
)
|
||||
}
|
||||
|
||||
const dayTemplate = (DateHelper) => {
|
||||
const ROWS_COUNT = 7
|
||||
const ALLOWED_DOMAIN_TYPE = ['month']
|
||||
|
||||
return {
|
||||
name: 'ghDayFix',
|
||||
allowedDomainType: ALLOWED_DOMAIN_TYPE,
|
||||
rowsCount: () => ROWS_COUNT,
|
||||
columnsCount: (ts) => {
|
||||
let count = DateHelper.getWeeksCountInMonth(ts)
|
||||
const endOfMonth = moment().endOf('month').toDate()
|
||||
const clampEnd = DateHelper.getFirstWeekOfMonth(endOfMonth).toDate()
|
||||
|
||||
if(moment(ts).isSame(new Date(), 'month') && endOfMonth > clampEnd) {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
},
|
||||
mapping: (startTimestamp, endTimestamp) => {
|
||||
const clampStart = DateHelper.getFirstWeekOfMonth(startTimestamp)
|
||||
let clampEnd = DateHelper.getFirstWeekOfMonth(endTimestamp)
|
||||
|
||||
if(moment(startTimestamp).isSame(new Date(), 'month')){
|
||||
clampEnd = DateHelper.date().add(1, 'day')
|
||||
}
|
||||
|
||||
let x = -1
|
||||
const pivotDay = clampStart.weekday()
|
||||
|
||||
return DateHelper.intervals('day', clampStart, clampEnd).map((ts) => {
|
||||
const weekday = DateHelper.date(ts).weekday()
|
||||
if (weekday === pivotDay) {
|
||||
x += 1
|
||||
}
|
||||
|
||||
return {
|
||||
t: ts,
|
||||
x,
|
||||
y: weekday,
|
||||
}
|
||||
})
|
||||
},
|
||||
extractUnit: (ts) => DateHelper.date(ts).startOf('day').valueOf(),
|
||||
}
|
||||
}
|
||||
|
||||
export function renderCal(el: HTMLElement, d: { date: any, value: any }[]): Promise<any> {
|
||||
const cal = new CalHeatmap()
|
||||
cal.addTemplates(dayTemplate)
|
||||
return cal.paint({
|
||||
itemSelector: el,
|
||||
domain: {
|
||||
type: 'month',
|
||||
label: { text: 'MMM', textAlign: 'start', position: 'top' },
|
||||
},
|
||||
subDomain: {
|
||||
type: 'ghDayFix',
|
||||
radius: 2, width: 11, height: 11, gutter: 4
|
||||
},
|
||||
range: 12,
|
||||
data: { source: d.filter(x => x.value > 0), x: 'date', y: 'value' },
|
||||
scale: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
range: [ '#14432a', '#4dd05a' ],
|
||||
domain: [ 0, d.reduce((a, b) => Math.max(a, b.value), 0) ]
|
||||
},
|
||||
},
|
||||
date: { start: moment().subtract(1, 'year').add(1, 'month').toDate() },
|
||||
theme: 'dark',
|
||||
}, [
|
||||
[ CalTooltip, {
|
||||
text: (_: Date, v: number, d: any) =>
|
||||
`${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}`
|
||||
} ]
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
const now = moment()
|
||||
export const CHARTJS_OPT: ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
// TODO: Show point on hover
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
title: (tooltipItems) => {
|
||||
const date = tooltipItems[0].parsed.x
|
||||
const diff = now.diff(date, 'days')
|
||||
return diff ? `${diff} days ago` : 'Today'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const pfpNotFound = (e: Event) => (e.target as HTMLImageElement).src = DEFAULT_PFP
|
||||
export const coverNotFound = (e: Event) => (e.target as HTMLImageElement).src = "/assets/imgs/no_cover.jpg"
|
||||
|
||||
|
||||
/**
|
||||
* use:tooltip
|
||||
*/
|
||||
export function tooltip(element: HTMLElement, params: { text: string, dom: HTMLElement } | string | HTMLElement) {
|
||||
// Create div if not exists
|
||||
if (!document.querySelector('.aqua-tooltip')) {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('aqua-tooltip')
|
||||
|
||||
// Initially hidden
|
||||
div.style.display = 'none'
|
||||
|
||||
document.body.appendChild(div)
|
||||
}
|
||||
|
||||
let isFocus = false
|
||||
let div: HTMLDivElement = document.querySelector('.aqua-tooltip')!
|
||||
const p: string = typeof params === 'string' ? params
|
||||
: 'dom' in params ? params.dom.outerHTML
|
||||
: params.outerHTML
|
||||
|
||||
function updatePosition(event: MouseEvent) {
|
||||
div.style.top = `${event.pageY + 10}px`
|
||||
div.style.left = `${event.pageX - div.clientWidth / 2 + 5}px`
|
||||
}
|
||||
|
||||
function mouseOver(event: MouseEvent) {
|
||||
if (isFocus) return
|
||||
div.innerHTML = p
|
||||
div.style.display = ''
|
||||
updatePosition(event)
|
||||
isFocus = true
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
isFocus = false
|
||||
div.style.display = 'none'
|
||||
}
|
||||
|
||||
element.addEventListener('mouseover', mouseOver)
|
||||
element.addEventListener('mouseleave', mouseLeave)
|
||||
element.addEventListener('mousemove', updatePosition)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
element.removeEventListener('mouseover', mouseOver)
|
||||
element.removeEventListener('mouseleave', mouseLeave)
|
||||
element.removeEventListener('mousemove', updatePosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function pfp(node: HTMLImageElement, me?: AquaNetUser) {
|
||||
node.src = me?.profilePicture ? `${AQUA_HOST}/uploads/net/portrait/${me.profilePicture}` : DEFAULT_PFP
|
||||
node.onerror = e => pfpNotFound(e as Event)
|
||||
}
|
||||
7
AquaNet/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { mount } from 'svelte';
|
||||
import './app.sass'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, { target: document.getElementById("app")! });
|
||||
|
||||
export default app
|
||||
98
AquaNet/src/pages/Home.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import LinkCard from "./Home/LinkCard.svelte";
|
||||
import SetupInstructions from "./Home/SetupInstructions.svelte";
|
||||
import { DISCORD_INVITE, FADE_IN, FADE_OUT } from "../libs/config";
|
||||
import { USER } from "../libs/sdk.js";
|
||||
import type { AquaNetUser } from "../libs/generalTypes";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
import ActionCard from "../components/ActionCard.svelte";
|
||||
import { t } from "../libs/i18n";
|
||||
import ImportDataAction from "./Home/ImportDataAction.svelte";
|
||||
import Communities from "./Home/Communities.svelte";
|
||||
|
||||
USER.ensureLoggedIn();
|
||||
|
||||
let me: AquaNetUser
|
||||
let error = ""
|
||||
|
||||
let tab = 0;
|
||||
let tabs = [t('home.nav.portal'), t('home.nav.link-card'), t('home.nav.game-setup')]
|
||||
|
||||
USER.me().then((m) => me = m).catch(e => error = e.message)
|
||||
</script>
|
||||
|
||||
<main class="content">
|
||||
<!-- <h2 class="outer-title"> </h2>-->
|
||||
<nav class="tabs">
|
||||
{#each tabs as t, i}
|
||||
<div class="clickable"
|
||||
class:active={tab === i}
|
||||
on:click={() => tab = i}
|
||||
on:keydown={(e) => e.key === "Enter" && (tab = i)}
|
||||
role="button" tabindex={i}>{t}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
{#if tab === 0}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="action-cards">
|
||||
<ActionCard color="255, 192, 203" icon="solar:card-bold-duotone" on:click={() => tab = 1}>
|
||||
{#if me && me.cards.length > 1}
|
||||
<h3>{t('home.manage-cards')}</h3>
|
||||
<span>{t('home.manage-cards-description')}</span>
|
||||
{:else if me}
|
||||
<h3>{t('home.link-card')}</h3>
|
||||
<span>{t('home.link-cards-description')}</span>
|
||||
{/if}
|
||||
</ActionCard>
|
||||
|
||||
<ActionCard color="82, 93, 233" icon="fluent:chat-12-filled" on:click={() => tab = 3}>
|
||||
<h3>{t('home.join-community')}</h3>
|
||||
<span>{t('home.join-community-description')}</span>
|
||||
</ActionCard>
|
||||
|
||||
<ActionCard on:click={() => tab = 2} icon="uil:link-alt">
|
||||
<h3>{t('home.setup')}</h3>
|
||||
<span>{t('home.setup-description')}</span>
|
||||
</ActionCard>
|
||||
|
||||
<ImportDataAction/>
|
||||
</div>
|
||||
{:else if tab === 1}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<LinkCard/>
|
||||
</div>
|
||||
{:else if tab === 2}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<SetupInstructions/>
|
||||
</div>
|
||||
{:else if tab === 3}
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<Communities/>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<StatusOverlays {error} loading={!me}/>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.tabs
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
div
|
||||
&.active
|
||||
color: vars.$c-main
|
||||
|
||||
h3
|
||||
font-size: 1.3rem
|
||||
margin: 0
|
||||
|
||||
.action-cards
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
</style>
|
||||
26
AquaNet/src/pages/Home/Communities.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from "../../libs/i18n";
|
||||
import CommunityCard from "../../components/CommunityCard.svelte";
|
||||
import { DISCORD_INVITE, QQ_INVITE, TELEGRAM_INVITE } from "../../libs/config";
|
||||
</script>
|
||||
|
||||
<div class="setup-instructions">
|
||||
<h2>{t('home.join-community')}</h2>
|
||||
<div class="grid cols-3 gap-4">
|
||||
<CommunityCard color="82, 93, 233" icon="ic:baseline-discord" on:click={() => window.location.href = DISCORD_INVITE}>
|
||||
<h3>Discord</h3>
|
||||
</CommunityCard>
|
||||
<CommunityCard color="46, 163, 224" icon="mingcute:telegram-fill" on:click={() => window.location.href = TELEGRAM_INVITE}>
|
||||
<h3>Telegram</h3>
|
||||
</CommunityCard>
|
||||
<CommunityCard color="226, 60, 68" icon="ri:qq-fill" on:click={() => window.location.href = QQ_INVITE}>
|
||||
<h3>QQ</h3>
|
||||
</CommunityCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
</style>
|
||||
162
AquaNet/src/pages/Home/ImportDataAction.svelte
Normal file
@@ -0,0 +1,162 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition"
|
||||
import { t } from "../../libs/i18n";
|
||||
import ActionCard from "../../components/ActionCard.svelte";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import { CARD, GAME, USER } from "../../libs/sdk";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let load = false;
|
||||
let error = "";
|
||||
let conflict: {
|
||||
oldName: string,
|
||||
oldRating: number,
|
||||
newName: string,
|
||||
newRating: number
|
||||
} | null;
|
||||
let confirmAction: (override: boolean) => void;
|
||||
|
||||
let fileInput: HTMLInputElement;
|
||||
const startImport = async (e: Event & { currentTarget: EventTarget & HTMLInputElement; }) => {
|
||||
const file = e.currentTarget.files?.[0]
|
||||
|
||||
if (!file) return;
|
||||
load = true;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(await file.text()) as any;
|
||||
const me = await USER.me();
|
||||
|
||||
const maybeUserMusicList = data?.userMusicList || data;
|
||||
if (Array.isArray(maybeUserMusicList) && maybeUserMusicList.every(it => Array.isArray(it?.userMusicDetailList))) {
|
||||
// Is music list array
|
||||
await GAME.importMusicDetail("mai2", maybeUserMusicList.flatMap(it => it.userMusicDetailList));
|
||||
location.href = `/u/${me.username}/mai2`;
|
||||
return;
|
||||
}
|
||||
|
||||
const game = getGameByCode(data.gameId);
|
||||
const userGames = await CARD.userGames(me.username);
|
||||
|
||||
const existed = userGames[game];
|
||||
if (existed) {
|
||||
conflict = {
|
||||
oldName: existed.name,
|
||||
oldRating: existed.rating,
|
||||
newName: data.userData.userName,
|
||||
newRating: data.userData.playerRating
|
||||
};
|
||||
if (!await new Promise(resolve => confirmAction = resolve)) {
|
||||
return;
|
||||
}
|
||||
conflict = null;
|
||||
}
|
||||
|
||||
await GAME.import(game, data);
|
||||
location.href = `/u/${me.username}/${game}`;
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
console.error(e);
|
||||
} finally {
|
||||
conflict = null;
|
||||
load = false;
|
||||
}
|
||||
}
|
||||
|
||||
const getGameByCode = (code: string) => {
|
||||
switch (code?.toUpperCase()) {
|
||||
case 'SDEZ':
|
||||
return 'mai2';
|
||||
case 'SDHD':
|
||||
return 'chu3';
|
||||
default:
|
||||
throw new Error(t('home.import.unknown-game'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionCard color="209, 124, 102" icon="bxs:file-import" on:click={() => fileInput.click()}>
|
||||
<h3>{t('home.import')}</h3>
|
||||
<span>{t('home.import-description')}</span>
|
||||
<input type="file" accept=".json" bind:this={fileInput} style="display: none"
|
||||
on:change={startImport}/>
|
||||
</ActionCard>
|
||||
|
||||
<StatusOverlays {error} loading={load}/>
|
||||
|
||||
{#if conflict}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2>{t('home.import.data-conflict')}</h2>
|
||||
<p></p>
|
||||
<div class="conflict-cards">
|
||||
<div class="old card">
|
||||
<span class="type">{t('home.linkcard.account-card')}</span>
|
||||
<span>{t('home.linkcard.name')}: {conflict.oldName}</span>
|
||||
<span>{t('home.linkcard.rating')}: {conflict.oldRating}</span>
|
||||
<div class="trash">
|
||||
<Icon icon="ph:trash-duotone"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<Icon icon="icon-park-outline:down"/>
|
||||
</div>
|
||||
<div class="new card">
|
||||
<span class="type">{t('home.import.new-data')}</span>
|
||||
<span>{t('home.linkcard.name')}: {conflict.newName}</span>
|
||||
<span>{t('home.linkcard.rating')}: {conflict.newRating}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="buttons">
|
||||
<button on:click={() => confirmAction(false)}>{t('action.cancel')}</button>
|
||||
<button class="error" on:click={() => confirmAction(true)}>{t('action.confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@use "../../vars"
|
||||
h3
|
||||
font-size: 1.3rem
|
||||
margin: 0
|
||||
|
||||
.conflict-cards
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
|
||||
gap: 0.5rem
|
||||
align-items: center
|
||||
|
||||
span:not(.type)
|
||||
font-size: 0.8rem
|
||||
|
||||
.old
|
||||
background: #ff6b6b20
|
||||
border: 1px solid vars.$c-error
|
||||
color: #ffffff99
|
||||
position: relative
|
||||
|
||||
.trash
|
||||
display: flex
|
||||
position: absolute
|
||||
bottom: 0.5rem
|
||||
right: 0.5rem
|
||||
color: vars.$c-error
|
||||
opacity: 0.6
|
||||
font-size: 2rem
|
||||
|
||||
.new
|
||||
background: #646cff20
|
||||
border: 1px solid vars.$c-darker
|
||||
|
||||
.buttons
|
||||
display: grid
|
||||
grid-template-columns: 1fr 1fr
|
||||
gap: 1rem
|
||||
|
||||
.icon
|
||||
display: flex
|
||||
justify-content: center
|
||||
font-size: 2rem
|
||||
</style>
|
||||
375
AquaNet/src/pages/Home/LinkCard.svelte
Normal file
@@ -0,0 +1,375 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition"
|
||||
import type { Card, CardSummary, CardSummaryGame, ConfirmProps, AquaNetUser } from "../../libs/generalTypes";
|
||||
import { CARD, USER } from "../../libs/sdk";
|
||||
import moment from "moment"
|
||||
import Icon from "@iconify/svelte";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import { t } from "../../libs/i18n";
|
||||
|
||||
// State
|
||||
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"
|
||||
let showConfirm: ConfirmProps | null = null
|
||||
|
||||
let error: string = ""
|
||||
let me: AquaNetUser | null = null
|
||||
let accountCardSummary: CardSummary | null = null
|
||||
|
||||
// Fetch data for current user
|
||||
const updateMe = () => USER.me().then(m => {
|
||||
me = m
|
||||
m.cards.sort((a, b) => a.registerTime < b.registerTime ? 1 : -1)
|
||||
CARD.summary(m.ghostCard.luid).then(s => accountCardSummary = s.summary)
|
||||
|
||||
// Always put the ghost card at the top
|
||||
m.cards.sort((a, b) => a.isGhost ? -1 : 1)
|
||||
state = "ready"
|
||||
}).catch(e => error = e.message)
|
||||
updateMe()
|
||||
|
||||
// Data conflict overlay
|
||||
let conflictCardID: string = ""
|
||||
let conflictSummary: CardSummary | null = null
|
||||
let conflictGame: string = ""
|
||||
let conflictNew: CardSummaryGame | null = null
|
||||
let conflictOld: CardSummaryGame | null = null
|
||||
let conflictToMigrate: string[] = []
|
||||
|
||||
function setError(msg: string, type: 'AC' | 'SN') {
|
||||
type === 'AC' ? errorAC = msg : errorSN = msg
|
||||
}
|
||||
|
||||
async function doLink(id: string, migrate: string) {
|
||||
await CARD.link({cardId: id, migrate})
|
||||
await updateMe()
|
||||
state = "ready"
|
||||
}
|
||||
|
||||
async function link(type: 'AC' | 'SN') {
|
||||
if (state !== 'ready' || accountCardSummary === null) return
|
||||
state = "linking-" + type
|
||||
const id = type === 'AC' ? inputAC : inputSN
|
||||
|
||||
console.log("linking card", id)
|
||||
|
||||
// Check if this card is already linked in the account
|
||||
if (me?.cards?.some(c => formatLUID(c.luid, c.isGhost).toLowerCase() === id.toLowerCase())) {
|
||||
setError(t('home.linkcard.linked-own'), type)
|
||||
state = "ready"
|
||||
return
|
||||
}
|
||||
|
||||
// First, lookup the card summary
|
||||
const card = (await CARD.summary(id).catch(e => {
|
||||
// If card is not found, create a card and link it
|
||||
if (e.message === t('home.linkcard.notfound')) {
|
||||
doLink(id, "")
|
||||
return
|
||||
}
|
||||
|
||||
setError(e.message, type)
|
||||
state = "ready"
|
||||
return
|
||||
}))!
|
||||
const summary = card.summary
|
||||
|
||||
// Check if it's already linked
|
||||
if (card.card.linked) {
|
||||
setError(t('home.linkcard.linked-another'), type)
|
||||
state = "ready"
|
||||
return
|
||||
}
|
||||
|
||||
// If all games in summary are null or doesn't conflict with the ghost card,
|
||||
// we can link the card directly
|
||||
if (Object.keys(summary).every(k => summary[k as keyof CardSummary] === null
|
||||
|| accountCardSummary!![k as keyof CardSummary] === null)) {
|
||||
console.log("linking card directly")
|
||||
await doLink(id, Object.keys(summary).filter(k => summary[k as keyof CardSummary] !== null).join(","))
|
||||
}
|
||||
|
||||
// For each conflicting game, ask the user if they want to migrate the data
|
||||
else {
|
||||
conflictSummary = summary
|
||||
conflictCardID = id
|
||||
await linkConflictContinue(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function linkConflictContinue(choose: "old" | "new" | null) {
|
||||
if (accountCardSummary === null || conflictSummary === null) return
|
||||
console.log("linking card with migration")
|
||||
|
||||
if (choose) {
|
||||
// If old is chosen, nothing needs to be migrated
|
||||
// If new is chosen, we need to migrate the data
|
||||
if (choose === "new") {
|
||||
conflictToMigrate.push(conflictGame)
|
||||
}
|
||||
// Continue to the next card
|
||||
conflictSummary[conflictGame as keyof CardSummary] = null
|
||||
}
|
||||
|
||||
let isConflict = false
|
||||
for (const k in conflictSummary) {
|
||||
conflictNew = conflictSummary[k as keyof CardSummary]
|
||||
conflictOld = accountCardSummary[k as keyof CardSummary]
|
||||
conflictGame = k
|
||||
if (!conflictNew || !conflictOld) continue
|
||||
|
||||
isConflict = true
|
||||
break
|
||||
}
|
||||
|
||||
// If there are no longer conflicts, we can link the card
|
||||
if (!isConflict) {
|
||||
await doLink(conflictCardID, conflictToMigrate.join(","))
|
||||
|
||||
// Reset the conflict state
|
||||
linkConflictCancel()
|
||||
}
|
||||
}
|
||||
|
||||
function linkConflictCancel() {
|
||||
state = "ready"
|
||||
conflictSummary = null
|
||||
conflictCardID = ""
|
||||
conflictGame = ""
|
||||
conflictNew = null
|
||||
conflictOld = null
|
||||
conflictToMigrate = []
|
||||
}
|
||||
|
||||
async function unlink(card: Card) {
|
||||
showConfirm = {
|
||||
title: t('home.linkcard.unlink'),
|
||||
message: t('home.linkcard.unlink-notice'),
|
||||
confirm: async () => {
|
||||
await CARD.unlink(card.luid)
|
||||
await updateMe()
|
||||
showConfirm = null
|
||||
},
|
||||
cancel: () => showConfirm = null,
|
||||
dangerous: true
|
||||
}
|
||||
}
|
||||
|
||||
// Access code input
|
||||
const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/
|
||||
let inputAC = ""
|
||||
let errorAC = ""
|
||||
|
||||
function inputACChange(e: any) {
|
||||
e = e as InputEvent
|
||||
// Add spaces to the input
|
||||
const old = inputAC
|
||||
if (e.inputType === "insertText" && inputAC.length % 5 === 4 && inputAC.length < 24)
|
||||
inputAC += " "
|
||||
inputAC = inputAC.slice(0, 24)
|
||||
if (inputAC !== old) errorAC = ""
|
||||
}
|
||||
|
||||
// Serial number input
|
||||
const inputSNRegex = /^([0-9A-Fa-f]{0,2}:){0,7}[0-9A-Fa-f]{0,2}$/
|
||||
let inputSN = ""
|
||||
let errorSN = ""
|
||||
|
||||
function inputSNChange(e: any) {
|
||||
e = e as InputEvent
|
||||
// Add colons to the input
|
||||
const old = inputSN
|
||||
if (e.inputType === "insertText" && inputSN.length % 3 === 2 && inputSN.length < 23)
|
||||
inputSN += ":"
|
||||
inputSN = inputSN.toUpperCase().slice(0, 23)
|
||||
if (inputSN !== old) errorSN = ""
|
||||
}
|
||||
|
||||
function formatLUID(luid: string, ghost: boolean = false) {
|
||||
if (ghost) return luid.slice(0, 6) + " " + (luid.slice(6).match(/.{4}/g)?.join(" ") ?? "")
|
||||
switch (cardType(luid)) {
|
||||
case "Felica SN":
|
||||
return BigInt(luid).toString(16).toUpperCase().padStart(16, "0").match(/.{1,2}/g)!.join(":")
|
||||
case "Access Code":
|
||||
return luid.match(/.{4}/g)!.join(" ")
|
||||
default:
|
||||
return luid
|
||||
}
|
||||
}
|
||||
|
||||
function cardType(luid: string) {
|
||||
if (luid.startsWith("00")) return "Felica SN"
|
||||
if (luid.length === 20) return "Access Code"
|
||||
if (luid.includes(":")) return "Felica SN"
|
||||
if (luid.includes(" ")) return "Access Code"
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
function isInput(e: KeyboardEvent) {
|
||||
return e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="link-card">
|
||||
<h2>{t('home.linkcard.cards')}</h2>
|
||||
<p>{t('home.linkcard.description')}:</p>
|
||||
|
||||
{#if me}
|
||||
<div class="existing-cards" transition:slide>
|
||||
{#each me.cards as card (card.luid)}
|
||||
<div class:ghost={card.isGhost} class='existing card' transition:fade|global>
|
||||
<span class="type">{card.isGhost ? t('home.linkcard.account-card') : cardType(card.luid)}</span>
|
||||
<span class="register">{t('home.linkcard.registered')}: {moment(card.registerTime).format("YYYY MMM DD")}</span>
|
||||
<span class="last">{t('home.linkcard.lastused')}: {moment(card.accessTime).format("YYYY MMM DD")}</span>
|
||||
<div></div>
|
||||
<span class="id">{formatLUID(card.luid, card.isGhost)}</span>
|
||||
{#if !card.isGhost}
|
||||
<button class="icon error" on:click={() => unlink(card)}><Icon icon="tabler:trash-x-filled"/></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2>{t('home.link-card')}</h2>
|
||||
<p>{t('home.linkcard.enter-info')}:</p>
|
||||
{#if !inputSN}
|
||||
<div out:slide={{ duration: 250 }}>
|
||||
<p>{t('home.linkcard.access-code')}</p>
|
||||
<label>
|
||||
<!-- DO NOT change the order of bind:value and on:input. Their order determines the order of reactivity -->
|
||||
<input placeholder="e.g. 5200 1234 5678 9012 3456"
|
||||
on:keydown={(e) => {
|
||||
e.key === "Enter" && link('AC')
|
||||
// Ensure key is numeric
|
||||
if (isInput(e) && !/[\d ]/.test(e.key)) e.preventDefault()
|
||||
}}
|
||||
bind:value={inputAC}
|
||||
on:input={inputACChange}
|
||||
class:error={inputAC && (!inputACRegex.test(inputAC) || errorAC)}>
|
||||
{#if inputAC.length > 0}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => {link('AC');inputAC=''}}>{t('home.linkcard.link')}</button>
|
||||
{/if}
|
||||
</label>
|
||||
{#if errorAC}
|
||||
<p class="error" transition:slide>{errorAC}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !inputAC}
|
||||
<div out:slide={{ duration: 250 }}>
|
||||
<p>{t('home.linkcard.enter-sn1')}
|
||||
(<a href="https://play.google.com/store/apps/details?id=com.wakdev.wdnfc">Android</a> /
|
||||
<a href="https://apps.apple.com/us/app/nfc-tools/id1252962749">Apple</a>)
|
||||
{t('home.linkcard.enter-sn2')}
|
||||
</p>
|
||||
<label>
|
||||
<input placeholder="e.g. 01:2E:1A:2B:3C:4D:5E:6F"
|
||||
on:keydown={(e) => {
|
||||
e.key === "Enter" && link('SN')
|
||||
// Ensure key is hex or colon
|
||||
if (isInput(e) && !/[0-9A-Fa-f:]/.test(e.key)) e.preventDefault()
|
||||
}}
|
||||
bind:value={inputSN}
|
||||
on:input={inputSNChange}
|
||||
class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}>
|
||||
{#if inputSN.length > 0}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => {link('SN'); inputSN = ''}}>{t('home.linkcard.link')}</button>
|
||||
{/if}
|
||||
</label>
|
||||
{#if errorSN}
|
||||
<p class="error" transition:slide>{errorSN}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if conflictOld && conflictNew && me}
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2>{t('home.linkcard.data-conflict')}</h2>
|
||||
<p></p>
|
||||
<div class="conflict-cards">
|
||||
<div class="old card clickable" on:click={() => linkConflictContinue('old')}
|
||||
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('old')}>
|
||||
<span class="type">{t('home.linkcard.account-card')}</span>
|
||||
<span>{t('home.linkcard.name')}: {conflictOld.name}</span>
|
||||
<span>{t('home.linkcard.rating')}: {conflictOld.rating}</span>
|
||||
<span>{t('home.linkcard.last-login')}: {moment(conflictOld.lastLogin).format("YYYY MMM DD")}</span>
|
||||
<span class="id">{formatLUID(me.ghostCard.luid, true)}</span>
|
||||
</div>
|
||||
<div class="new card clickable" on:click={() => linkConflictContinue('new')}
|
||||
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('new')}>
|
||||
<span class="type">{cardType(conflictCardID)}</span>
|
||||
<span>{t('home.linkcard.name')}: {conflictNew.name}</span>
|
||||
<span>{t('home.linkcard.rating')}: {conflictNew.rating}</span>
|
||||
<span>{t('home.linkcard.last-login')}: {moment(conflictNew.lastLogin).format("YYYY MMM DD")}</span>
|
||||
<span class="id">{conflictCardID}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="error" on:click={linkConflictCancel}>{t('action.cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays bind:confirm={showConfirm} bind:error={error} loading={!me} />
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../../vars"
|
||||
|
||||
.link-card
|
||||
input
|
||||
width: 100%
|
||||
|
||||
label
|
||||
display: flex
|
||||
|
||||
button
|
||||
margin-left: 1rem
|
||||
|
||||
.existing-cards, .conflict-cards
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
|
||||
gap: 1rem
|
||||
|
||||
.existing-cards .existing.card
|
||||
min-height: 90px
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
*
|
||||
white-space: nowrap
|
||||
|
||||
&.ghost
|
||||
background: rgba(vars.$c-darker, 0.8)
|
||||
|
||||
.register, .last
|
||||
opacity: 0.7
|
||||
|
||||
span:not(.type)
|
||||
font-size: 0.8rem
|
||||
|
||||
> div
|
||||
flex: 1
|
||||
|
||||
button
|
||||
position: absolute
|
||||
right: 10px
|
||||
bottom: 10px
|
||||
|
||||
.conflict-cards
|
||||
.card
|
||||
transition: vars.$transition
|
||||
|
||||
.card:hover
|
||||
background: vars.$c-darker
|
||||
|
||||
span:not(.type)
|
||||
font-size: 0.8rem
|
||||
|
||||
.id
|
||||
opacity: 0.7
|
||||
|
||||
</style>
|
||||
103
AquaNet/src/pages/Home/SetupInstructions.svelte
Normal file
@@ -0,0 +1,103 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { USER } from "../../libs/sdk";
|
||||
import type { AquaNetUser } from "../../libs/generalTypes";
|
||||
import { codeToHtml } from 'shiki'
|
||||
import { AQUA_CONNECTION, DISCORD_INVITE, FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import { t } from "../../libs/i18n";
|
||||
|
||||
let user: AquaNetUser
|
||||
let keychip: string;
|
||||
let keychipCode: string;
|
||||
let getStartedRequesting = false;
|
||||
|
||||
USER.me().then((u) => {
|
||||
user = u;
|
||||
});
|
||||
|
||||
function getStarted() {
|
||||
if (getStartedRequesting) return;
|
||||
|
||||
getStartedRequesting = true;
|
||||
USER.keychip().then(k => {
|
||||
getStartedRequesting = false;
|
||||
keychip = k;
|
||||
codeToHtml(`
|
||||
[dns]
|
||||
default=${AQUA_CONNECTION}
|
||||
|
||||
[keychip]
|
||||
enable=1
|
||||
; ${t('home.setup.keychip-tips')}
|
||||
id=${keychip.slice(0, 4)}-${keychip.slice(4)}1337`.trim(), {
|
||||
lang: 'ini',
|
||||
theme: 'rose-pine',
|
||||
}).then((html) => {
|
||||
keychipCode = html;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="setup-instructions">
|
||||
<h2>{t('home.setup')}</h2>
|
||||
<p>
|
||||
{t('home.setup.welcome')}
|
||||
</p>
|
||||
<blockquote>
|
||||
{t('home.setup.blockquote')}
|
||||
</blockquote>
|
||||
|
||||
{#if user}
|
||||
<div transition:slide>
|
||||
{#if !keychip && !keychipCode}
|
||||
<div class="no-margin" out:fade={FADE_OUT}>
|
||||
<button class="emp" on:click={getStarted}>{t('home.setup.get')}</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-margin" in:fade={FADE_IN}>
|
||||
<p>
|
||||
{t('home.setup.edit')}:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
{@html keychipCode}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{t('home.setup.test')}
|
||||
</p>
|
||||
<p>
|
||||
{t('home.setup.ask')} <a href={DISCORD_INVITE}>Discord</a> {t('home.setup.support')}.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.code
|
||||
overflow-x: auto
|
||||
|
||||
:global(pre.shiki)
|
||||
background-color: transparent !important
|
||||
|
||||
:global(code)
|
||||
counter-reset: step
|
||||
counter-increment: step 0
|
||||
|
||||
:global(code .line::before)
|
||||
content: counter(step)
|
||||
counter-increment: step
|
||||
width: 1rem
|
||||
margin-right: 1.5rem
|
||||
display: inline-block
|
||||
text-align: right
|
||||
color: rgba(115,138,148,.4)
|
||||
|
||||
</style>
|
||||
136
AquaNet/src/pages/Ranking.svelte
Normal file
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { title } from "../libs/ui";
|
||||
import { GAME } from "../libs/sdk";
|
||||
import type { GenericRanking } from "../libs/generalTypes";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
import type { GameName } from "../libs/scoring";
|
||||
import { GAME_TITLE } from "../libs/i18n";
|
||||
import { t } from "../libs/i18n";
|
||||
import UserCard from "../components/UserCard.svelte";
|
||||
import Tooltip from "../components/Tooltip.svelte";
|
||||
|
||||
export let game: GameName = 'mai2';
|
||||
|
||||
title(`Ranking`);
|
||||
|
||||
let d: { users: GenericRanking[] };
|
||||
let error: string | null;
|
||||
Promise.all([GAME.ranking(game)])
|
||||
.then(([users]) => {
|
||||
console.log(users)
|
||||
d = { users };
|
||||
})
|
||||
.catch((e) => error = e.message);
|
||||
|
||||
let hoveringUser = "";
|
||||
let hoverLoading = false;
|
||||
</script>
|
||||
|
||||
<main class="content leaderboard">
|
||||
<div class="outer-title-options">
|
||||
<h2>{t("Leaderboard.Title")}</h2>
|
||||
<nav>
|
||||
{#each Object.entries(GAME_TITLE) as [k, v]}
|
||||
<a href="/ranking/{k}" class:active={k === game}>{v}</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if d}
|
||||
<div class="leaderboard-container">
|
||||
<div class="lb-user" on:mouseenter={() => hoveringUser = d.users[0].username} role="heading" aria-level="2">
|
||||
<span class="rank">{t("Leaderboard.Rank")}</span>
|
||||
<span class="name"></span>
|
||||
<span class="rating">{t("Leaderboard.Rating")}</span>
|
||||
<span class="accuracy">{t("Leaderboard.Accuracy")}</span>
|
||||
<span class="fc">{t("Leaderboard.FC")}</span>
|
||||
<span class="ap">{t("Leaderboard.AP")}</span>
|
||||
</div>
|
||||
{#each d.users as user, i (user.rank)}
|
||||
<div class="lb-user" class:alternate={i % 2 === 1} role="listitem"
|
||||
on:mouseover={() => hoveringUser = user.username} on:focus={() => {}}>
|
||||
|
||||
<span class="rank">#{user.rank}</span>
|
||||
<span class="name">
|
||||
{#if user.username !== ""}
|
||||
<a href="/u/{user.username}/{game}" class:registered={!(/user\d+/.test(user.username))}>{user.name}</a>
|
||||
{:else}
|
||||
<span>{user.name}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="rating">{
|
||||
game === 'chu3' ?
|
||||
(user.rating / 100).toFixed(2) :
|
||||
user.rating.toLocaleString()
|
||||
}</span>
|
||||
<span class="accuracy">{(+user.accuracy).toFixed(2)}%</span>
|
||||
<span class="fc">{user.fullCombo}</span>
|
||||
<span class="ap">{user.allPerfect}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Tooltip triggeredBy=".name" loading={hoverLoading}>
|
||||
<UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} />
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays error={error} loading={!d} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.leaderboard-container
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.lb-user
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
width: 100%
|
||||
gap: 12px
|
||||
border-radius: vars.$border-radius
|
||||
padding: 6px 12px
|
||||
box-sizing: border-box
|
||||
|
||||
> *:not(.name)
|
||||
text-align: center
|
||||
|
||||
.name
|
||||
min-width: 100px
|
||||
flex: 1
|
||||
|
||||
> a
|
||||
color: unset
|
||||
|
||||
.registered
|
||||
background: vars.$grad-special
|
||||
color: transparent
|
||||
-webkit-background-clip: text
|
||||
background-clip: text
|
||||
|
||||
.accuracy, .rating
|
||||
width: 15%
|
||||
min-width: 45px
|
||||
|
||||
.rating
|
||||
font-weight: bold
|
||||
color: white
|
||||
|
||||
.fc, .ap
|
||||
width: 5%
|
||||
min-width: 20px
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
font-size: 0.9rem
|
||||
|
||||
.accuracy
|
||||
display: none
|
||||
|
||||
&.alternate
|
||||
background-color: vars.$ov-light
|
||||
|
||||
|
||||
</style>
|
||||
200
AquaNet/src/pages/User/Settings.svelte
Normal file
@@ -0,0 +1,200 @@
|
||||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import type { AquaNetUser } from "../../libs/generalTypes";
|
||||
import { CARD, USER } from "../../libs/sdk";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { pfp } from "../../libs/ui";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import UserBox from "../../components/settings/ChuniSettings.svelte";
|
||||
import Mai2Settings from "../../components/settings/Mai2Settings.svelte";
|
||||
import WaccaSettings from "../../components/settings/WaccaSettings.svelte";
|
||||
import GeneralGameSettings from "../../components/settings/GeneralGameSettings.svelte";
|
||||
|
||||
USER.ensureLoggedIn()
|
||||
|
||||
let me: AquaNetUser;
|
||||
let error: string;
|
||||
let submitting = ""
|
||||
let tab = 0
|
||||
let tabs = [ 'profile', 'game' ]
|
||||
|
||||
const profileFields = [
|
||||
[ 'displayName', t('settings.profile.name') ],
|
||||
[ 'username', t('settings.profile.username') ],
|
||||
[ 'password', t('settings.profile.password') ],
|
||||
[ 'profileLocation', t('settings.profile.location') ],
|
||||
[ 'profileBio', t('settings.profile.bio') ],
|
||||
] as const
|
||||
|
||||
// Fetch user data
|
||||
const getMe = () => USER.me().then((m) => {
|
||||
me = m
|
||||
|
||||
CARD.userGames(m.username).then(games => {
|
||||
if (games.chu3 && !tabs.includes('chu3')) {
|
||||
tabs = [...tabs, 'chu3']
|
||||
}
|
||||
if (games.mai2 && !tabs.includes('mai2')) {
|
||||
tabs = [...tabs, 'mai2']
|
||||
}
|
||||
if (games.wacca && !tabs.includes('wacca')) {
|
||||
tabs = [...tabs, 'wacca']
|
||||
}
|
||||
})
|
||||
}).catch(e => error = e.message)
|
||||
getMe()
|
||||
|
||||
let changed: string[] = []
|
||||
let pfpField: HTMLInputElement
|
||||
|
||||
function submit(field: string, value: string) {
|
||||
if (submitting) return
|
||||
submitting = field
|
||||
|
||||
USER.setting(field, value).then(() => {
|
||||
changed = changed.filter(c => c !== field)
|
||||
}).catch(e => error = e.message).finally(() => submitting = "")
|
||||
}
|
||||
|
||||
function uploadPfp(file: File) {
|
||||
if (submitting) return
|
||||
submitting = 'profilePicture'
|
||||
|
||||
USER.uploadPfp(file).then(() => {
|
||||
me.profilePicture = file.name
|
||||
// reload
|
||||
getMe()
|
||||
}).catch(e => error = e.message).finally(() => submitting = "")
|
||||
}
|
||||
|
||||
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
|
||||
if (whether) node.type = 'password'
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="content">
|
||||
<div class="outer-title-options">
|
||||
<h2>{t('settings.title')}</h2>
|
||||
<nav>
|
||||
{#each tabs as tabName, i}
|
||||
<div transition:slide={{axis: 'x'}} class:active={tab === i}
|
||||
on:click={() => tab = i} on:keydown={e => e.key === 'Enter' && (tab = i)}
|
||||
role="button" tabindex="0">
|
||||
{ts(`settings.tabs.${tabName}`)}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if tab === 0 && me}
|
||||
<!-- Tab 0: Profile settings -->
|
||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
||||
<div class="field">
|
||||
<label for="profile-upload">{t('settings.profile.picture')}</label>
|
||||
<div>
|
||||
{#if me && me.profilePicture}
|
||||
<div on:click={() => pfpField.click()} on:keydown={e => e.key === 'Enter' && pfpField.click()}
|
||||
role="button" tabindex="0" class="clickable">
|
||||
<img use:pfp={me} alt="Profile" />
|
||||
</div>
|
||||
{:else}
|
||||
<button on:click={() => pfpField.click()}>
|
||||
{t('settings.profile.upload-new')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<input id="profile-upload" type="file" accept="image/*" style="display: none" bind:this={pfpField}
|
||||
on:change={() => pfpField.files && uploadPfp(pfpField.files[0])} />
|
||||
</div>
|
||||
|
||||
{#each profileFields as [field, name], i (field)}
|
||||
<div class="field">
|
||||
<label for={field}>{name}</label>
|
||||
<div>
|
||||
<input id={field} type="text" use:passwordAction={field === 'password'}
|
||||
bind:value={me[field]} on:input={() => changed = [...changed, field]}
|
||||
placeholder={field === 'password' ? t('settings.profile.unchanged') : t('settings.profile.unset')}/>
|
||||
{#if changed.includes(field) && me[field]}
|
||||
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, me[field])}>
|
||||
{#if submitting === field}
|
||||
<Icon icon="line-md:loading-twotone-loop" />
|
||||
{:else}
|
||||
{t('settings.profile.save')}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="field m-t">
|
||||
<div class="bool">
|
||||
<input id="optOutOfLeaderboard" type="checkbox" bind:checked={me.optOutOfLeaderboard}
|
||||
on:change={() => submit('optOutOfLeaderboard', me.optOutOfLeaderboard.toString())}/>
|
||||
<label for="optOutOfLeaderboard">
|
||||
<span class="name">{ts(`settings.fields.optOutOfLeaderboard.name`)}</span>
|
||||
<span class="desc">{ts(`settings.fields.optOutOfLeaderboard.desc`)}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if tabs[tab] === 'chu3'}
|
||||
<!-- Userbox settings -->
|
||||
<UserBox />
|
||||
{:else if tabs[tab] === 'mai2'}
|
||||
<Mai2Settings username={me.username} />
|
||||
{:else if tabs[tab] === 'wacca'}
|
||||
<WaccaSettings />
|
||||
{:else if tabs[tab] === 'game'}
|
||||
<GeneralGameSettings />
|
||||
{/if}
|
||||
|
||||
<StatusOverlays {error} loading={!me || !!submitting} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../../vars"
|
||||
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
|
||||
.bool
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.desc
|
||||
opacity: 0.6
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> input
|
||||
flex: 1
|
||||
|
||||
img
|
||||
max-width: 100px
|
||||
max-height: 100px
|
||||
border-radius: vars.$border-radius
|
||||
object-fit: cover
|
||||
|
||||
</style>
|
||||
579
AquaNet/src/pages/UserHome.svelte
Normal file
@@ -0,0 +1,579 @@
|
||||
<script lang="ts">
|
||||
import { CHARTJS_OPT, coverNotFound, pfpNotFound, registerChart, renderCal, title, tooltip, pfp } from "../libs/ui";
|
||||
import type {
|
||||
GenericGamePlaylog,
|
||||
GenericGameSummary,
|
||||
MusicMeta,
|
||||
TrendEntry,
|
||||
AquaNetUser,
|
||||
|
||||
AllMusic
|
||||
|
||||
} from "../libs/generalTypes";
|
||||
import { DATA_HOST } from "../libs/config";
|
||||
import 'cal-heatmap/cal-heatmap.css';
|
||||
import moment from "moment";
|
||||
import 'chartjs-adapter-moment';
|
||||
import { CARD, DATA, GAME, USER } from "../libs/sdk";
|
||||
import { type GameName, getMult, roundFloor } from "../libs/scoring";
|
||||
import StatusOverlays from "../components/StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { GAME_TITLE, t } from "../libs/i18n";
|
||||
import RankDetails from "../components/RankDetails.svelte";
|
||||
import RatingComposition from "../components/RatingComposition.svelte";
|
||||
import useLocalStorage from "../libs/hooks/useLocalStorage.svelte";
|
||||
import Line from "../components/chart/Line.svelte";
|
||||
|
||||
const TREND_DAYS = 60
|
||||
|
||||
registerChart()
|
||||
|
||||
export let username: string;
|
||||
export let game: GameName = "mai2"
|
||||
let calElement: HTMLElement
|
||||
let error: string;
|
||||
let me: AquaNetUser
|
||||
title(`User ${username}`)
|
||||
const rounding = useLocalStorage("rounding", true);
|
||||
|
||||
const titleText = GAME_TITLE[game]
|
||||
|
||||
interface MusicAndPlay extends MusicMeta, GenericGamePlaylog {}
|
||||
|
||||
let d: {
|
||||
user: GenericGameSummary,
|
||||
trend: TrendEntry[]
|
||||
recent: MusicAndPlay[],
|
||||
validGames: [ string, string ][],
|
||||
} | null
|
||||
|
||||
let allMusics: AllMusic
|
||||
let showDetailRank = false
|
||||
let isLoading = false
|
||||
|
||||
function init() {
|
||||
USER.isLoggedIn() && USER.me().then(u => me = u)
|
||||
|
||||
CARD.userGames(username).then(games => {
|
||||
if (!games[game]) {
|
||||
// Find a valid game
|
||||
const valid = Object.entries(games).filter(([g, valid]) => valid)
|
||||
if (!valid || !valid[0]) return error = t("UserHome.NoValidGame")
|
||||
window.location.href = `/u/${username}/${valid[0][0]}`
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
GAME.userSummary(username, game),
|
||||
GAME.trend(username, game),
|
||||
DATA.allMusic(game),
|
||||
]).then(([user, trend, music]) => {
|
||||
console.log(user)
|
||||
console.log(trend)
|
||||
console.log(games)
|
||||
|
||||
// If game is wacca, divide all ratings by 10
|
||||
if (game === 'wacca') {
|
||||
user.rating /= 10
|
||||
trend.forEach(it => it.rating /= 10)
|
||||
user.recent.forEach(it => {
|
||||
it.beforeRating /= 10
|
||||
it.afterRating /= 10
|
||||
})
|
||||
}
|
||||
|
||||
const minDate = moment().subtract(TREND_DAYS, 'days').format("YYYY-MM-DD")
|
||||
d = {user,
|
||||
trend: trend.filter(it => it.date >= minDate && it.plays != 0),
|
||||
recent: user.recent.map(it => {return {...music[it.musicId], ...it}}),
|
||||
validGames: Object.entries(GAME_TITLE).filter(g => games[g[0] as GameName])
|
||||
}
|
||||
allMusics = music
|
||||
renderCal(calElement, trend.map(it => {return {date: it.date, value: it.plays}})).then(() => {
|
||||
// Scroll to the rightmost
|
||||
calElement.scrollLeft = calElement.scrollWidth - calElement.clientWidth
|
||||
})
|
||||
}).catch((e) => error = e.message);
|
||||
}).catch((e) => { error = e.message; console.error(e) } );
|
||||
}
|
||||
|
||||
if (Object.keys(GAME_TITLE).includes(game)) init()
|
||||
else error = t("UserHome.InvalidGame", {game})
|
||||
|
||||
const setRival = (isAdd: boolean) => {
|
||||
isLoading = true
|
||||
GAME.setRival(game, username, isAdd).then(() => {
|
||||
d!.user.rival = isAdd
|
||||
}).catch(e => error = e.message).finally(() => isLoading = false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<main id="user-home" class="content">
|
||||
{#if d}
|
||||
<div class="user-pfp">
|
||||
<img use:pfp={d.user.aquaUser} alt="" class="pfp" on:error={pfpNotFound}>
|
||||
<div class="name-box">
|
||||
<h2>{d.user.name}</h2>
|
||||
{#if typeof d.user.rival === 'boolean' && game === 'mai2'}
|
||||
<span class="clickable" on:click={() => setRival(!d?.user.rival)} role="button" tabindex="0"
|
||||
on:keydown={e => e.key === "Enter" && setRival(!d?.user.rival)}>
|
||||
{d.user.rival ? t("UserHome.RemoveRival") : t("UserHome.AddRival")}
|
||||
</span>
|
||||
{/if}
|
||||
{#if me && me.username === username}
|
||||
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href="/settings">
|
||||
<Icon icon="eos-icons:rotating-gear"/>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<nav>
|
||||
{#each d.validGames as [g, name]}
|
||||
<a href={`/u/${username}/${g}`} class:active={game === g}>{name}</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>{titleText} {t('UserHome.Statistics')}</h2>
|
||||
<div class="scoring-info">
|
||||
<div class="chart">
|
||||
<div class="info-top">
|
||||
<div class="rating">
|
||||
<span>{game === 'mai2' ? t("UserHome.DXRating"): t("UserHome.Rating")}</span>
|
||||
<span>{
|
||||
game === 'chu3' ?
|
||||
(d.user.rating / 100).toFixed(2) :
|
||||
d.user.rating.toLocaleString()
|
||||
}</span>
|
||||
</div>
|
||||
|
||||
<div class="rank">
|
||||
<span>{t('UserHome.ServerRank')}</span>
|
||||
<span>#{+d.user.serverRank.toLocaleString() + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trend">
|
||||
<!-- ChartJS cannot be fully responsive unless there is a parent div that's independent from its size and helps it determine its size -->
|
||||
<div class="chartjs-box-reference">
|
||||
{#if d.trend.length <= 1}
|
||||
<div class="no-data">{t("UserHome.NoData", { days: TREND_DAYS })}</div>
|
||||
{:else}
|
||||
<Line data={{
|
||||
datasets: [
|
||||
{
|
||||
label: 'Rating',
|
||||
data: d.trend.map(it => {return {x: Date.parse(it.date), y: it.rating}}),
|
||||
borderColor: '#646cff',
|
||||
tension: 0.1,
|
||||
|
||||
// TODO: Set X axis span to 3 months
|
||||
}
|
||||
]
|
||||
}} options={CHARTJS_OPT} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if Object.entries(d.user.detailedRanks).length > 0}
|
||||
<div class="info-bottom clickable" use:tooltip={t("UserHome.ShowRanksDetails")}
|
||||
on:click={() => showDetailRank = !showDetailRank} role="button" tabindex="0"
|
||||
on:keydown={e => e.key === "Enter" && (showDetailRank = !showDetailRank)}>
|
||||
{#each d.user.ranks as r}
|
||||
<div><span>{r.name}</span><span>{r.count}</span></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="info-bottom">
|
||||
{#each d.user.ranks as r}
|
||||
<div><span>{r.name}</span><span>{r.count}</span></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="other-info">
|
||||
<div class="accuracy">
|
||||
<span>{t('UserHome.Accuracy')}</span>
|
||||
<span>{(d.user.accuracy).toFixed(2)}%</span>
|
||||
</div>
|
||||
|
||||
<div class="max-combo">
|
||||
<span>{t("UserHome.MaxCombo")}</span>
|
||||
<span>{d.user.maxCombo}</span>
|
||||
</div>
|
||||
|
||||
<div class="full-combo">
|
||||
<span>{t("UserHome.FullCombo")}</span>
|
||||
<span>{d.user.fullCombo}</span>
|
||||
</div>
|
||||
|
||||
<div class="all-perfect">
|
||||
<span>{t("UserHome.AllPerfect")}</span>
|
||||
<span>{d.user.allPerfect}</span>
|
||||
</div>
|
||||
|
||||
<div class="total-dx-score">
|
||||
<span>{game === 'mai2' ? t('UserHome.DXScore') : t("UserHome.Score")}</span>
|
||||
<span>{d.user.totalScore.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDetailRank}<RankDetails g={d.user}/>{/if}
|
||||
|
||||
<div>
|
||||
<h2>{t('UserHome.PlayActivity')}</h2>
|
||||
<div class="activity-info">
|
||||
<div class="hide-scrollbar" id="cal-heatmap" bind:this={calElement}></div>
|
||||
|
||||
<div class="info-bottom">
|
||||
<div class="plays">
|
||||
<span>{t("UserHome.Plays")}</span>
|
||||
<span>{d.user.plays}</span>
|
||||
</div>
|
||||
|
||||
<div class="time">
|
||||
<span>{t('UserHome.PlayTime')}</span>
|
||||
<span>{(d.user.totalPlayTime / 60).toFixed(1)} hr</span>
|
||||
</div>
|
||||
|
||||
<div class="first-play">
|
||||
<span>{t('UserHome.FirstSeen')}</span>
|
||||
<span>{moment(d.user.joined).format("YYYY-MM-DD")}</span>
|
||||
</div>
|
||||
|
||||
<div class="last-play">
|
||||
<span>{t('UserHome.LastSeen')}</span>
|
||||
<span>{moment(d.user.lastSeen).format("YYYY-MM-DD")}</span>
|
||||
</div>
|
||||
|
||||
<div class="last-version">
|
||||
<span>{t('UserHome.Version')}</span>
|
||||
<span>{d.user.lastVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RatingComposition title="B30" comp={d.user.ratingComposition.best30} {allMusics} {game}/>
|
||||
<RatingComposition title="B35" comp={d.user.ratingComposition.best35} {allMusics} {game}/>
|
||||
<RatingComposition title="B15" comp={d.user.ratingComposition.best15} {allMusics} {game}/>
|
||||
<!-- <RatingComposition title="Hot 10" comp={d.user.ratingComposition.hot10} {allMusics} {game}/> -->
|
||||
<!-- <RatingComposition title="N10" comp={d.user.ratingComposition.next10} {allMusics} {game}/> -->
|
||||
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} {game} top={10}/>
|
||||
|
||||
<div class="recent">
|
||||
<h2>{t('UserHome.RecentScores')}</h2>
|
||||
<div class="scores">
|
||||
{#each d.recent as r, i}
|
||||
<div class:alt={i % 2 === 0}>
|
||||
<img src={`${DATA_HOST}/d/${game}/music/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`} alt="" on:error={coverNotFound} />
|
||||
<div class="info">
|
||||
<div>{r.name ?? t("UserHome.UnknownSong")}</div>
|
||||
<div>
|
||||
{#if r.isAllPerfect || r.isAllJustice}
|
||||
<img src="/assets/imgs/All Perfect.png" alt="All Perfect" />
|
||||
{:else if r.isFullCombo}
|
||||
<img src="/assets/imgs/Full Combo.png" alt="Full Combo" />
|
||||
{/if}
|
||||
<span class={`lv level-${r.level === 10 ? 5 : r.level}`}>
|
||||
<span>
|
||||
{r.notes?.[r.level === 10 ? 0 : r.level]?.lv?.toFixed(1) ?? r.worldsEndTag ?? '-'}
|
||||
</span>
|
||||
</span>
|
||||
<span class={`rank-${getMult(r.achievement, game)[2].toString()[0]}`}>
|
||||
<span class="rank-text">{("" + getMult(r.achievement, game)[2]).replace("p", "+")}</span>
|
||||
<span class="rank-num" use:tooltip={(r.achievement / 10000).toFixed(4)}>
|
||||
{
|
||||
rounding.value ?
|
||||
roundFloor(r.achievement, game, 1) :
|
||||
(r.achievement / 10000).toFixed(4)
|
||||
}%
|
||||
</span>
|
||||
</span>
|
||||
{#if game === 'mai2' || game === 'wacca'}
|
||||
<span class:increased={r.afterRating - r.beforeRating > 0} class="dx-change">
|
||||
{r.afterRating === r.beforeRating ? '-' : (r.afterRating - r.beforeRating).toFixed(0)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<StatusOverlays {error} loading={!d || isLoading} />
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
#user-home
|
||||
.user-pfp
|
||||
display: flex
|
||||
align-items: flex-end
|
||||
gap: vars.$gap
|
||||
margin-top: -72px
|
||||
position: relative
|
||||
|
||||
h2
|
||||
font-size: 2rem
|
||||
margin: 0
|
||||
white-space: nowrap
|
||||
|
||||
nav
|
||||
position: absolute
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 10px
|
||||
top: 4px
|
||||
right: 0
|
||||
|
||||
.setting-icon
|
||||
font-size: 1.5rem
|
||||
color: vars.$c-main
|
||||
cursor: pointer
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.name-box
|
||||
flex: 1
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
gap: 10px
|
||||
|
||||
.pfp
|
||||
width: 100px
|
||||
height: 100px
|
||||
border-radius: vars.$border-radius
|
||||
object-fit: cover
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
.user-pfp
|
||||
margin-top: -68px
|
||||
h2
|
||||
font-size: 1.5rem
|
||||
|
||||
.pfp
|
||||
width: 80px
|
||||
height: 80px
|
||||
|
||||
.info-bottom, .info-top, .other-info
|
||||
display: flex
|
||||
gap: vars.$gap
|
||||
|
||||
> div
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
> span:first-child
|
||||
font-weight: bold
|
||||
font-size: 0.8rem
|
||||
|
||||
// character spacing
|
||||
letter-spacing: 0.1em
|
||||
color: vars.$c-main
|
||||
|
||||
.info-bottom
|
||||
width: max-content
|
||||
|
||||
.info-top > div > span:last-child
|
||||
font-size: 1.5rem
|
||||
|
||||
.info-bottom
|
||||
max-width: 100%
|
||||
flex-wrap: wrap
|
||||
row-gap: 0.5rem
|
||||
|
||||
.scoring-info
|
||||
display: flex
|
||||
gap: vars.$gap
|
||||
max-height: 250px
|
||||
|
||||
.chart
|
||||
flex: 0 1 790px
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.other-info
|
||||
flex: 1 0 100px
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
justify-content: space-between
|
||||
|
||||
.trend
|
||||
height: 300px
|
||||
width: 100%
|
||||
max-width: 790px
|
||||
|
||||
position: relative
|
||||
|
||||
> .chartjs-box-reference
|
||||
position: absolute
|
||||
inset: 0
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
.no-data
|
||||
opacity: 0.5
|
||||
user-select: none
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
flex-direction: column
|
||||
max-height: unset
|
||||
|
||||
.chart
|
||||
flex: 0
|
||||
|
||||
.trend
|
||||
max-height: 130px
|
||||
|
||||
.other-info
|
||||
> div
|
||||
flex-direction: row
|
||||
justify-content: space-between
|
||||
|
||||
.info-bottom
|
||||
justify-content: space-between
|
||||
|
||||
.activity-info
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: vars.$gap
|
||||
|
||||
#cal-heatmap
|
||||
overflow-x: auto
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
#cal-heatmap
|
||||
width: 100%
|
||||
|
||||
.info-bottom
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
width: 100%
|
||||
|
||||
> div
|
||||
flex-direction: row
|
||||
justify-content: space-between
|
||||
|
||||
// Recent Scores section
|
||||
.recent
|
||||
.scores
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-wrap: wrap
|
||||
gap: vars.$gap
|
||||
|
||||
> div.alt
|
||||
background-color: rgba(white, 0.03)
|
||||
border-radius: vars.$border-radius
|
||||
|
||||
// Image and song info
|
||||
> div
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: vars.$gap
|
||||
padding-right: 16px
|
||||
max-width: 100%
|
||||
box-sizing: border-box
|
||||
|
||||
img
|
||||
width: 50px
|
||||
height: 50px
|
||||
border-radius: vars.$border-radius
|
||||
object-fit: cover
|
||||
|
||||
// Song info and score
|
||||
> div.info
|
||||
flex: 1
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
overflow: hidden
|
||||
|
||||
// Limit song name to one line
|
||||
> div:first-child
|
||||
flex: 1
|
||||
min-width: 0
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
// Make song score and rank not wrap
|
||||
> div:last-child
|
||||
white-space: nowrap
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 10px
|
||||
|
||||
img
|
||||
height: 1.5em
|
||||
width: 1.5em
|
||||
|
||||
@media (max-width: vars.$w-mobile)
|
||||
flex-direction: column
|
||||
gap: 0
|
||||
|
||||
.rank-text
|
||||
text-align: left
|
||||
|
||||
// Score and rank should be space-between on mobile
|
||||
> div:last-child
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
gap: 10px
|
||||
|
||||
.lv
|
||||
margin-right: auto
|
||||
|
||||
.rank-S
|
||||
// Gold green gradient on text
|
||||
background: vars.$grad-special
|
||||
-webkit-background-clip: text
|
||||
color: transparent
|
||||
|
||||
.rank-A
|
||||
color: #ff8a8a
|
||||
|
||||
.rank-B
|
||||
color: #6ba6ff
|
||||
|
||||
.lv
|
||||
min-width: 30px
|
||||
text-align: center
|
||||
background: rgba(var(--lv-color), 0.6)
|
||||
padding: 0 6px
|
||||
border-radius: vars.$border-radius
|
||||
|
||||
.lv.level-5 > span
|
||||
color: transparent
|
||||
background: var(--lv-text-clip)
|
||||
background-clip: text
|
||||
-webkit-background-clip: text
|
||||
font-weight: bold
|
||||
font-size: 1em
|
||||
font-family: 'Arial Black', sans-serif
|
||||
|
||||
span
|
||||
display: inline-block
|
||||
text-align: right
|
||||
|
||||
// Vertical table-like alignment
|
||||
span.rank-text
|
||||
min-width: 38px
|
||||
span.rank-num
|
||||
min-width: 60px
|
||||
span.dx-change
|
||||
min-width: 30px
|
||||
|
||||
span.increased
|
||||
&:before
|
||||
content: "+"
|
||||
color: vars.$c-good
|
||||
</style>
|
||||
251
AquaNet/src/pages/Welcome.svelte
Normal file
@@ -0,0 +1,251 @@
|
||||
<script lang="ts">
|
||||
import { Turnstile } from "svelte-turnstile";
|
||||
import { slide } from 'svelte/transition';
|
||||
import { TURNSTILE_SITE_KEY } from "../libs/config";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { USER } from "../libs/sdk";
|
||||
import { t } from "../libs/i18n"
|
||||
|
||||
let params = new URLSearchParams(window.location.search)
|
||||
|
||||
let state = "home"
|
||||
$: isSignup = state === "signup"
|
||||
let submitting = false
|
||||
|
||||
let email = ""
|
||||
let password = ""
|
||||
let username = ""
|
||||
let turnstile = ""
|
||||
let turnstileReset: () => void | undefined;
|
||||
|
||||
let error = ""
|
||||
let verifyMsg = ""
|
||||
|
||||
if (params.get('confirm-email')) {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verifying")
|
||||
submitting = true
|
||||
|
||||
// Send request to server
|
||||
USER.confirmEmail(params.get('confirm-email')!)
|
||||
.then(() => {
|
||||
verifyMsg = t('welcome.verified')
|
||||
submitting = false
|
||||
|
||||
// Clear the query param
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
})
|
||||
.catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message }))
|
||||
}
|
||||
|
||||
async function submit(): Promise<any> {
|
||||
submitting = true
|
||||
|
||||
// Check if username and password are valid
|
||||
if (email === "" || password === "") {
|
||||
error = t("welcome.email-password-missing")
|
||||
return submitting = false
|
||||
}
|
||||
|
||||
if (turnstile === "") {
|
||||
// Sleep for 100ms to allow Turnstile to finish
|
||||
error = t("welcome.waiting-turnstile")
|
||||
return setTimeout(submit, 100)
|
||||
}
|
||||
|
||||
// Signup
|
||||
if (isSignup) {
|
||||
if (username === "") {
|
||||
error = t("welcome.username-missing")
|
||||
return submitting = false
|
||||
}
|
||||
|
||||
// Send request to server
|
||||
await USER.register({ username, email, password, turnstile })
|
||||
.then(() => {
|
||||
// Show verify email message
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verification-sent", { email })
|
||||
})
|
||||
.catch(e => {
|
||||
error = e.message
|
||||
submitting = false
|
||||
turnstileReset()
|
||||
})
|
||||
}
|
||||
else {
|
||||
// Send request to server
|
||||
await USER.login({ email, password, turnstile })
|
||||
.then(() => window.location.href = "/home")
|
||||
.catch(e => {
|
||||
if (e.message === 'Email not verified - STATE_0') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verify-state-0")
|
||||
}
|
||||
else if (e.message === 'Email not verified - STATE_1') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verify-state-1")
|
||||
}
|
||||
else if (e.message === 'Email not verified - STATE_2') {
|
||||
state = 'verify'
|
||||
verifyMsg = t("welcome.verify-state-2")
|
||||
}
|
||||
else {
|
||||
error = e.message
|
||||
submitting = false
|
||||
turnstileReset()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
submitting = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<main id="home" class="no-margin">
|
||||
<div>
|
||||
<h1 id="title">AquaNet</h1>
|
||||
{#if state === "home"}
|
||||
<div class="btn-group" transition:slide>
|
||||
<button on:click={() => state = 'login'}>{t('welcome.btn-login')}</button>
|
||||
<button on:click={() => state = 'signup'}>{t('welcome.btn-signup')}</button>
|
||||
</div>
|
||||
{:else if state === "login" || state === "signup"}
|
||||
<div class="login-form" transition:slide>
|
||||
{#if error}
|
||||
<span class="error">{error}</span>
|
||||
{/if}
|
||||
<div on:click={() => state = 'home'} on:keypress={() => state = 'home'}
|
||||
role="button" tabindex="0" class="clickable">
|
||||
<Icon icon="line-md:chevron-small-left" />
|
||||
<span>{t('back')}</span>
|
||||
</div>
|
||||
{#if isSignup}
|
||||
<input type="text" placeholder={t('username')} bind:value={username}>
|
||||
{/if}
|
||||
<input type="email" placeholder={t('email')} bind:value={email}>
|
||||
<input type="password" placeholder={t('password')} bind:value={password}>
|
||||
<button on:click={submit}>
|
||||
{#if submitting}
|
||||
<Icon icon="line-md:loading-twotone-loop"/>
|
||||
{:else}
|
||||
{isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')}
|
||||
{/if}
|
||||
</button>
|
||||
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
|
||||
on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
|
||||
on:turnstile-error={_ => console.log(error = t("welcome.turnstile-error"))}
|
||||
on:turnstile-expired={_ => window.location.reload()}
|
||||
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
|
||||
</div>
|
||||
{:else if state === "verify"}
|
||||
<div class="login-form" transition:slide>
|
||||
<span>{verifyMsg}</span>
|
||||
{#if !submitting}
|
||||
<button on:click={() => state = 'home'} transition:slide>{t('back')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="light-pollution">
|
||||
<div class="l1"></div>
|
||||
<div class="l2"></div>
|
||||
<div class="l3"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../vars"
|
||||
|
||||
.login-form
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
width: calc(100% - 12px)
|
||||
max-width: 300px
|
||||
|
||||
div.clickable
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
#home
|
||||
color: vars.$c-main
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
padding-left: 100px
|
||||
overflow: hidden
|
||||
background-color: black
|
||||
|
||||
box-sizing: border-box
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
|
||||
margin-top: -(vars.$nav-height)
|
||||
|
||||
// Content container
|
||||
> div
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: flex-start
|
||||
width: max-content
|
||||
|
||||
// Switching state container
|
||||
> div
|
||||
transition: vars.$transition
|
||||
|
||||
#title
|
||||
font-family: Quicksand, vars.$font
|
||||
user-select: none
|
||||
|
||||
// Gap between text characters
|
||||
letter-spacing: 0.2em
|
||||
margin-top: 0
|
||||
margin-bottom: 32px
|
||||
opacity: 0.9
|
||||
|
||||
.btn-group
|
||||
display: flex
|
||||
gap: 8px
|
||||
|
||||
.light-pollution
|
||||
pointer-events: none
|
||||
opacity: 0.8
|
||||
|
||||
> div
|
||||
position: absolute
|
||||
z-index: 1
|
||||
|
||||
.l1
|
||||
left: -560px
|
||||
top: 90px
|
||||
height: 1130px
|
||||
width: 1500px
|
||||
$color: rgb(158, 110, 230)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
|
||||
|
||||
.l2
|
||||
left: -200px
|
||||
top: 560px
|
||||
height: 1200px
|
||||
width: 1500px
|
||||
$color: rgb(92, 195, 250)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
|
||||
|
||||
.l3
|
||||
left: -600px
|
||||
opacity: 0.7
|
||||
top: -630px
|
||||
width: 1500px
|
||||
height: 1000px
|
||||
$color: rgb(230, 110, 156)
|
||||
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
|
||||
|
||||
@media (max-width: 500px)
|
||||
align-items: center
|
||||
padding-left: 0
|
||||
</style>
|
||||
24
AquaNet/src/vars.sass
Normal file
@@ -0,0 +1,24 @@
|
||||
$font: Quicksand, Inter, LXGW Wenkai, Microsoft YaHei, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Avenir, Helvetica, Arial, sans-serif
|
||||
$c-main: #b3c6ff
|
||||
$c-sub: rgba(0, 0, 0, 0.77)
|
||||
$c-good: #b3ffb9
|
||||
$c-darker: #646cff
|
||||
$c-bg: #242424
|
||||
$c-error: #ff6b6b
|
||||
$c-shadow: rgba(0, 0, 0, 0.1)
|
||||
|
||||
$ov-light: rgba(white, 0.04)
|
||||
$ov-lighter: rgba(white, 0.08)
|
||||
$ov-dark: rgba(black, 0.1)
|
||||
$ov-darker: rgba(black, 0.18)
|
||||
|
||||
$nav-height: 4rem
|
||||
$w-mobile: 560px
|
||||
$w-max: 900px
|
||||
|
||||
$grad-special: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94)
|
||||
|
||||
$border-radius: 12px
|
||||
$gap: 20px
|
||||
|
||||
$transition: all 0.25s
|
||||
3
AquaNet/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
declare const APP_VERSION: string;
|
||||
7
AquaNet/svelte.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
76
AquaNet/tools/migrate.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
def extract_and_migrate_sass(file_path):
|
||||
"""Extracts <style lang="sass"> block from a Svelte file, runs sass-migrator on it, and replaces the original style block."""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
|
||||
# Regex pattern to match <style lang="sass">...</style>
|
||||
style_pattern = re.compile(r'<style\s+lang\s*=\s*["\']sass["\']>(.*?)</style>', re.DOTALL)
|
||||
|
||||
# Extract all matching style blocks
|
||||
matches = list(style_pattern.finditer(content))
|
||||
|
||||
if not matches:
|
||||
print(f"No <style lang='sass'> block found in {file_path}")
|
||||
return
|
||||
|
||||
updated_content = content
|
||||
|
||||
for match in matches:
|
||||
original_style_block = match.group(0) # The full <style>...</style> block
|
||||
sass_content = match.group(1) # The content inside the <style> block
|
||||
|
||||
# Create a temporary file in the same directory as the .svelte file
|
||||
temp_file_path = pathlib.Path(file_path).parent / f"{pathlib.Path(file_path).stem}_temp.sass"
|
||||
|
||||
# Write the SASS content to the temporary file
|
||||
with open(temp_file_path, 'w', encoding='utf-8') as temp_file:
|
||||
temp_file.write(sass_content)
|
||||
|
||||
# Run the sass-migrator on the temporary file
|
||||
try:
|
||||
subprocess.run(['sass-migrator', 'module', str(temp_file_path)], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error while running sass-migrator on {file_path}: {e}")
|
||||
continue
|
||||
|
||||
# Read back the migrated content
|
||||
with open(temp_file_path, 'r', encoding='utf-8') as temp_file:
|
||||
migrated_sass_content = temp_file.read()
|
||||
|
||||
# Create the new <style> block with the migrated SASS content
|
||||
new_style_block = f'<style lang="sass">{migrated_sass_content}</style>'
|
||||
|
||||
# Replace the original style block with the new one
|
||||
updated_content = updated_content.replace(original_style_block, new_style_block)
|
||||
|
||||
# Remove the temporary file
|
||||
temp_file_path.unlink()
|
||||
|
||||
# Write the updated content back to the original file
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
file.write(updated_content)
|
||||
|
||||
print(f"Updated {file_path}")
|
||||
|
||||
def process_svelte_files(directory):
|
||||
"""Recursively processes all .svelte files in the given directory."""
|
||||
|
||||
svelte_files = pathlib.Path(directory).rglob("*.svelte")
|
||||
|
||||
for svelte_file in svelte_files:
|
||||
extract_and_migrate_sass(svelte_file)
|
||||
|
||||
def main():
|
||||
"""Main function to process all .svelte files in the current directory."""
|
||||
|
||||
current_directory = pathlib.Path(__file__).parent
|
||||
process_svelte_files(current_directory)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
20
AquaNet/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
AquaNet/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
11
AquaNet/uno.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
defineConfig, presetTypography,
|
||||
presetUno
|
||||
} from 'unocss';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetTypography()
|
||||
]
|
||||
});
|
||||
14
AquaNet/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import UnoCSS from '@unocss/svelte-scoped/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
UnoCSS(),
|
||||
svelte()
|
||||
],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
},
|
||||
})
|
||||
36
Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
# Use a multi-stage build to keep the image size small
|
||||
# Start with a Gradle image for building the project
|
||||
#FROM gradle:jdk21-alpine as builder
|
||||
FROM gradle:8.8.0-jdk21 as builder
|
||||
|
||||
# Copy the Gradle wrapper and configuration files separately to leverage Docker cache
|
||||
COPY --chown=gradle:gradle gradlew /home/gradle/
|
||||
COPY --chown=gradle:gradle gradle /home/gradle/gradle
|
||||
COPY --chown=gradle:gradle build.gradle.kts settings.gradle.kts /home/gradle/
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /home/gradle
|
||||
|
||||
# Replace CRLF with LF in gradlew to make it work on Linux
|
||||
RUN sed -i 's/\r$//' ./gradlew
|
||||
|
||||
# Download dependencies - cached if build.gradle.kts and settings.gradle.kts are unchanged
|
||||
RUN ./gradlew dependencies
|
||||
|
||||
# Copy the project source, this layer is rebuilt whenever a file has changed
|
||||
COPY --chown=gradle:gradle src /home/gradle/src
|
||||
|
||||
# Build the application
|
||||
RUN ./gradlew build -x test
|
||||
|
||||
# Start with a fresh image for the runtime
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
|
||||
# Set the deployment directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the built JAR from the builder image
|
||||
COPY --from=builder /home/gradle/build/libs/AquaDX-*.jar /app/
|
||||
|
||||
# The command to run the application
|
||||
CMD java -jar AquaDX-*.jar
|
||||
439
LICENSE
Normal file
@@ -0,0 +1,439 @@
|
||||
TL;DR: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en
|
||||
|
||||
Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
||||
("Public License"). To the extent this Public License may be
|
||||
interpreted as a contract, You are granted the Licensed Rights in
|
||||
consideration of Your acceptance of these terms and conditions, and the
|
||||
Licensor grants You such rights in consideration of benefits the
|
||||
Licensor receives from making the Licensed Material available under
|
||||
these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-NC-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution, NonCommercial, and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. NonCommercial means not primarily intended for or directed towards
|
||||
commercial advantage or monetary compensation. For purposes of
|
||||
this Public License, the exchange of the Licensed Material for
|
||||
other material subject to Copyright and Similar Rights by digital
|
||||
file-sharing or similar means is NonCommercial provided there is
|
||||
no payment of monetary compensation in connection with the
|
||||
exchange.
|
||||
|
||||
l. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
m. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
n. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part, for NonCommercial purposes only; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material for
|
||||
NonCommercial purposes only.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties, including when
|
||||
the Licensed Material is used other than for NonCommercial
|
||||
purposes.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-NC-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database for NonCommercial purposes
|
||||
only;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
111
README.md
@@ -1,54 +1,109 @@
|
||||
# Aqua Server
|
||||
Multipurpose game server powered by Spring Boot, for ALL.Net based games
|
||||
# AquaDX
|
||||
|
||||
This is a forked maintaining attempt of the [original Aqua server](https://dev.s-ul.net/NeumPhis/aqua)
|
||||
Multipurpose game server powered by Spring Boot, for ALL.Net-based games
|
||||
|
||||
This is an attempt to rebuild the [original Aqua server](https://dev.s-ul.net/NeumPhis/aqua)
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [AquaMai](https://github.com/MewoLab/AquaMai): A maimai DX mod that adds many features to the game.
|
||||
* [AquaNet](./AquaNet): A new web frontend for the modern age.
|
||||
|
||||
### Supported Games
|
||||
* CHUNITHM SUN (and below)
|
||||
* CHUNITHM Paradise Lost (and below)
|
||||
* Maimai DX Festival Plus (and below)
|
||||
* Card Maker (1.34)
|
||||
* Project DIVA Arcade Future Tone
|
||||
* O.N.G.E.K.I. bright memory (and below)
|
||||
|
||||
Below is a list of games supported by this server.
|
||||
|
||||
| Game | Ver | Codename | Thanks to |
|
||||
|----------------------------|------|---------------|--------------------------------------------|
|
||||
| SDHD: CHUNITHM (Chusan) | 2.27 | LUMINOUS PLUS | [@rinsama](https://github.com/mxihan) |
|
||||
| SDEZ: MaiMai DX | 1.40 | BUDDiES | [@肥宅虾哥](https://github.com/FeiZhaixiage) |
|
||||
| SDGA: MaiMai DX (International) | 1.45 | BUDDiES PLUS | [@Clansty](https://github.com/clansty) |
|
||||
| SDED: Card Maker | 1.39 | | [@Becods](https://github.com/Becods) |
|
||||
| SBZV: Project DIVA Arcade | 7.10 | Future Tone | |
|
||||
| SDDT: O.N.G.E.K.I. | 1.45 | bright MEMORY Act.3 | [@Gamer2097](https://github.com/Gamer2097) |
|
||||
| SDFE: Wacca (*ALPHA STAGE) | 3.07 | Reverse | |
|
||||
|
||||
> **News**: AquaDX just added Wacca support on Mar 29, 2024! Feel free to test it out, but expect bugs and issues.
|
||||
|
||||
Check out these docs for more information.
|
||||
* [Game specific notes](docs/game_specific_notes.md)
|
||||
* [Frequently asked questions](docs/frequently_asked_questions.md)
|
||||
|
||||
### Notes
|
||||
* Some game may require additional patches and these will not provided in this project and repository. You already found this, so you know where to find related resources too.
|
||||
* This repository may contain untested, experimental implementation for few games which I can't test properly. If you couldn't find your wanted game in the above list, do not expect support.
|
||||
* This server also provides a simple API for viewing play records and edit settings for some games.
|
||||
* Some games may require additional patches and these will not provided in this project and repository. You already found this, so you know where to find related resources too.
|
||||
* This repository may contain untested, experimental implementations for a few games which I can't test properly. If you couldn't find your wanted game in the above list, do not expect support.
|
||||
* This server also provides a simple API for viewing play records and editing settings for some games.
|
||||
|
||||
### Usage
|
||||
Requirements:
|
||||
* Java 17 or above (for running JAR)
|
||||
* Optional databases: MariaDB 10.6.x (recommended) or MySQL 8.0.x
|
||||
### Usage (V1 Developmental Preview)
|
||||
|
||||
Run `java -jar aqua-x.x.xx-RELEASE.jar`
|
||||
> [!NOTE]
|
||||
> AquaDX v1 is currently under heavy development.
|
||||
> If you were using SQLite Aqua before, it's not supported in AquaDX and the command below will create a new MariaDB database.
|
||||
> We're working on a migration guide, which will be released along with AquaDX v1 stable.
|
||||
|
||||
By default, Aqua will use sqlite and save user data in data/db.sqlite.
|
||||
1. Install [Docker](https://www.docker.com/get-started/) and [Git](https://git-scm.com/downloads)
|
||||
2. Run `git clone https://github.com/hykilpikonna/AquaDX` to clone this repo.
|
||||
3. Run `docker compose up` in the AquaDX folder.
|
||||
|
||||
If you want to use optional databases, edit configuration file then it will auto create the table and import some initial data.
|
||||
If you're getting BAD on title server checks after the docker server is up, please edit `config/application.properties`
|
||||
and change `allnet.server.host` to your LAN IP address (e.g. 192.168.0.?). You can find your LAN address using the `ipconfig` command on Windows or `ifconfig` on Linux.
|
||||
|
||||
### Updating Instructions
|
||||
|
||||
> [!NOTE]
|
||||
> Please back up your database before you update! Even though we want to avoid database issues as much as possible, it's still possible that unexpected things will happen.
|
||||
|
||||
Please run the commands below in the AquaDX folder to update:
|
||||
|
||||
```
|
||||
# Backup your database
|
||||
docker run --rm -it mariadb:latest mariadb-dump -h host.docker.internal --port 3369 --user=cat --password=meow main > backup.sql
|
||||
|
||||
# Pull the new repository
|
||||
git pull
|
||||
|
||||
# Run the updated version
|
||||
docker compose up
|
||||
```
|
||||
|
||||
### Usage (Stable Old Version)
|
||||
|
||||
> [!WARNING]
|
||||
> The instructions below is for the old version of AquaDX 0.0.47. This version does not support the latest features and games.
|
||||
|
||||
1. Install [Java 21 Temurin JDK](https://adoptium.net/temurin/releases/?version=21) (Please select your appropriate operating system)
|
||||
2. Download the latest `aqua-nightly.zip` from [Releases](https://github.com/hykilpikonna/AquaDX/releases).
|
||||
3. Extract the zip file to a folder.
|
||||
4. Run `java -jar aqua.jar` in the folder.
|
||||
|
||||
By default, Aqua will use SQLite and save user data in `data/db.sqlite`.
|
||||
|
||||
If you want to use optional databases, please edit the configuration file then it will auto-create the table and import some initial data.
|
||||
|
||||
### Configuration
|
||||
Configuration is saved in `config/application.properties`, spring loads this file automatically.
|
||||
|
||||
* The host and port of game title servers can be overritten in `allnet.server.host` and `allnet.server.port`. By default it will send the same host and port the client used the request this information.
|
||||
This will be send to the game at booting and being used by following request.
|
||||
* You can switch to MariaDB (or MySQL) database by commenting the Sqlite part.
|
||||
* For some game, you might need to change some game specific config entries.
|
||||
* The host and port of game title servers can be overwritten in `allnet.server.host` and `allnet.server.port`. By default it will send the same host and port the client used the request this information.
|
||||
This will be sent to the game at booting and being used by the following request.
|
||||
* You can switch to the MariaDB database by commenting the Sqlite part.
|
||||
* For some games, you might need to change some game-specific config entries.
|
||||
|
||||
### Building
|
||||
You need to install JDK on your system. However, you don't need to care about Gradle, as wrapper script is included.
|
||||
You need to install JDK on your system. However, you don't need to install Gradle separately, as the `gradlew` wrapper script is included.
|
||||
```
|
||||
gradlew clean build
|
||||
```
|
||||
The `build/libs` folder will contain an jar file.
|
||||
The `build/libs` folder will contain a jar file.
|
||||
|
||||
### Credit
|
||||
* **samnyan**: The creator and developer of the original Aqua server
|
||||
* **Akasaka Ryuunosuke** : providing all the DIVA protocol information
|
||||
* Dom Eori : Developer of forked Aqua server, from v0.0.17 and up
|
||||
* **Akasaka Ryuunosuke**: providing all the DIVA protocol information
|
||||
* Dom Eori: Developer of forked Aqua server, from v0.0.17 and up
|
||||
* All devs who contribute to the [MiniMe server](https://dev.s-ul.net/djhackers/minime)
|
||||
* All contributors by merge request, issues and other channels
|
||||
* All contributors by merge requests, issues and other channels
|
||||
|
||||
### License: [CC By-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
|
||||
|
||||
* **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
* **NonCommercial** — You may not use the material for commercial purposes.
|
||||
* **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
||||
|
||||