Compare commits
539 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67b453d413 | ||
|
|
05cd1f70d3 | ||
|
|
c0ae06e766 | ||
|
|
6edb7be310 | ||
|
|
191624df37 | ||
|
|
7a179fe511 | ||
|
|
8f087409d5 | ||
|
|
37f5b88049 | ||
|
|
1b9e48d9ef | ||
|
|
d646fad4f6 | ||
|
|
2075aec8f7 | ||
|
|
6c735caff0 | ||
|
|
7bc4905536 | ||
|
|
7a717fb3d1 | ||
|
|
0ba4ca03e1 | ||
|
|
63b2898a53 | ||
|
|
69303b389c | ||
|
|
67b18de8bd | ||
|
|
ee10b96dc0 | ||
|
|
45ddc7642b | ||
|
|
26f18b3667 | ||
|
|
5f83589ba7 | ||
|
|
1d9df10d4e | ||
|
|
03488fea78 | ||
|
|
0295094d87 | ||
|
|
b5e3151cce | ||
|
|
1148d27c4a | ||
|
|
79c54e4074 | ||
|
|
e58a0d6c99 | ||
|
|
f4f4cf0fad | ||
|
|
9692363c1d | ||
|
|
5e23df6040 | ||
|
|
0a5e4abec2 | ||
|
|
4474eff9e6 | ||
|
|
d930ed2f59 | ||
|
|
6bd82985b7 | ||
|
|
1b6d6a45af | ||
|
|
2fafed94d2 | ||
|
|
4426b54f65 | ||
|
|
f0828a87d9 | ||
|
|
bd754fcb69 | ||
|
|
ce71191856 | ||
|
|
4f1165d59b | ||
|
|
0512993ba8 | ||
|
|
d61246086e | ||
|
|
328e47bfea | ||
|
|
e8579eb1dd | ||
|
|
531a72a8e8 | ||
|
|
0f3915f655 | ||
|
|
dd3ba29a5c | ||
|
|
8348aec931 | ||
|
|
ee1f3bf3eb | ||
|
|
732091c86b | ||
|
|
8f65ebec5a | ||
|
|
2c56bfeb87 | ||
|
|
c77226dcb0 | ||
|
|
044d44ebe1 | ||
|
|
5df62b8c7e | ||
|
|
2196931cfa | ||
|
|
42032bb889 | ||
|
|
a4c3f96528 | ||
|
|
0636c0a567 | ||
|
|
f35bdfa7f4 | ||
|
|
56271f6fc4 | ||
|
|
69b4c8bb27 | ||
|
|
775717b297 | ||
|
|
481f116b28 | ||
|
|
eb6e8d261f | ||
|
|
5b43037927 | ||
|
|
2a3d1fe56d | ||
|
|
1424d5856e | ||
|
|
e94cc8abe0 | ||
|
|
80198d0d5d | ||
|
|
541f1b4727 | ||
|
|
5dfdbc0aa3 | ||
|
|
c238d687e9 | ||
|
|
b4d33509bf | ||
|
|
5d9f0a70a2 | ||
|
|
7887357cb9 | ||
|
|
30b7218d43 | ||
|
|
e65c8c6e38 | ||
|
|
db16732bd0 | ||
|
|
daed931602 | ||
|
|
40131ad9c8 | ||
|
|
c54b33d95d | ||
|
|
57b158e614 | ||
|
|
416a81ef3b | ||
|
|
bd6e046155 | ||
|
|
5790bdfb40 | ||
|
|
c98aeb8c60 | ||
|
|
3ac567af50 | ||
|
|
43f8d564e6 | ||
|
|
b6fe0ad393 | ||
|
|
783d7dba1b | ||
|
|
7d44727626 | ||
|
|
a4c5ed492c | ||
|
|
1cab0c9515 | ||
|
|
81ee2f52de | ||
|
|
57da011daa | ||
|
|
d9da22c043 | ||
|
|
2126bb1fe7 | ||
|
|
31d43ff315 | ||
|
|
33dbbc0476 | ||
|
|
c6087c3c37 | ||
|
|
46090e430c | ||
|
|
0dd5287acf | ||
|
|
47167b9709 | ||
|
|
5bd567deb9 | ||
|
|
a875f3cfb1 | ||
|
|
5b2e0f7828 | ||
|
|
6eb2bedcb3 | ||
|
|
3f503d66c3 | ||
|
|
f5437546ec | ||
|
|
a9085d9fbf | ||
|
|
bbab060ef7 | ||
|
|
44670d9c79 | ||
|
|
0ac2043cff | ||
|
|
6587dbec86 | ||
|
|
b6a7a96d18 | ||
|
|
56ee9d2f37 | ||
|
|
b482013b25 | ||
|
|
87d88ddfb7 | ||
|
|
3f080cb8f9 | ||
|
|
17c8a2dbe2 | ||
|
|
58c4a86f39 | ||
|
|
e43b3ac766 | ||
|
|
1e341b4a51 | ||
|
|
0941555d68 | ||
|
|
f67f642341 | ||
|
|
2eb936b853 | ||
|
|
908d3ea26a | ||
|
|
f78700e5b8 | ||
|
|
8f1bdd3234 | ||
|
|
6c033c72eb | ||
|
|
8bc01b98e0 | ||
|
|
ed66a65757 | ||
|
|
62c4b0ccc8 | ||
|
|
d27e0722d2 | ||
|
|
b7bb5ea493 | ||
|
|
3f1b749537 | ||
|
|
85e3163bd9 | ||
|
|
0287a6c100 | ||
|
|
dfd69ddc28 | ||
|
|
2d87e6f731 | ||
|
|
e373a7ec62 | ||
|
|
05c5306343 | ||
|
|
0573851fd3 | ||
|
|
ea6891fd92 | ||
|
|
e94581ffa8 | ||
|
|
324dcc5f6c | ||
|
|
65ad316813 | ||
|
|
d4f1469d41 | ||
|
|
f83ad3f3af | ||
|
|
1566b1714a | ||
|
|
ae739fc218 | ||
|
|
f9a094a10d | ||
|
|
6f3cfb812a | ||
|
|
e12b442e9a | ||
|
|
658967a86b | ||
|
|
9cc9b075a9 | ||
|
|
df7598b640 | ||
|
|
427bc7bc73 | ||
|
|
368eb7727d | ||
|
|
d16c028bed | ||
|
|
873d52e368 | ||
|
|
a90a10699b | ||
|
|
daa6fc9354 | ||
|
|
bc4e31abc9 | ||
|
|
565c95d72d | ||
|
|
84839123b9 | ||
|
|
1a180c535d | ||
|
|
8e0c13abdd | ||
|
|
000ffc4f70 | ||
|
|
639e70391d | ||
|
|
68bf8bfe12 | ||
|
|
4b72a844e9 | ||
|
|
7920ad3733 | ||
|
|
2737b75d5b | ||
|
|
2514119313 | ||
|
|
303b7e0eb3 | ||
|
|
4c98871006 | ||
|
|
5c1bdfb798 | ||
|
|
f5a7234ff6 | ||
|
|
f7a3d6bf96 | ||
|
|
8701aea3a5 | ||
|
|
5b09c5d195 | ||
|
|
9392008873 | ||
|
|
f617271496 | ||
|
|
cec3b82c07 | ||
|
|
f6764f1d85 | ||
|
|
f38011faac | ||
|
|
8666b8d5ef | ||
|
|
1e7b665e3f | ||
|
|
1b1eb70b39 | ||
|
|
4b9460370d | ||
|
|
8ec7d86e79 | ||
|
|
e3878c3f76 | ||
|
|
ce973295f4 | ||
|
|
a82e77524f | ||
|
|
baf184771b | ||
|
|
564bf8f8a6 | ||
|
|
c5476d9dde | ||
|
|
d9f363ec4b | ||
|
|
2d3da9e9a7 | ||
|
|
5b30600d8b | ||
|
|
4f34d4ccbd | ||
|
|
1261893130 | ||
|
|
fdcbee07fa | ||
|
|
93efa65e27 | ||
|
|
1ddc81cd65 | ||
|
|
64015da127 | ||
|
|
cbd01abef5 | ||
|
|
3fdf85234d | ||
|
|
e0ad97e96d | ||
|
|
18519d932c | ||
|
|
bd60088dc8 | ||
|
|
6c9f2dd807 | ||
|
|
d78a3a93b8 | ||
|
|
36313c0127 | ||
|
|
75ae3a22cb | ||
|
|
4a040acb62 | ||
|
|
f439ef7f10 | ||
|
|
59205cf9bf | ||
|
|
85a9150be1 | ||
|
|
94b33388b2 | ||
|
|
15f1e523b4 | ||
|
|
45959408c5 | ||
|
|
e7994d2644 | ||
|
|
e259ec9dd4 | ||
|
|
c3870b30f3 | ||
|
|
1882465588 | ||
|
|
5e35289ae4 | ||
|
|
c518d51431 | ||
|
|
375665a90f | ||
|
|
7e8ad29cbc | ||
|
|
4fe3df2a80 | ||
|
|
a5a3a18486 | ||
|
|
ae014e46f2 | ||
|
|
a4ae9496a3 | ||
|
|
965395bf92 | ||
|
|
ccb836d790 | ||
|
|
c51e8fa1d9 | ||
|
|
ce9089dcee | ||
|
|
973eecabe9 | ||
|
|
71670a8d2c | ||
|
|
bad65cfe10 | ||
|
|
1f62ea966c | ||
|
|
2febdb4616 | ||
|
|
5dff2fa91e | ||
|
|
c15ce66bb8 | ||
|
|
b01b26bf86 | ||
|
|
0098bf1047 | ||
|
|
2e2670f275 | ||
|
|
89526d19b9 | ||
|
|
ac9eac2948 | ||
|
|
9fe0fecb0a | ||
|
|
6f786e68eb | ||
|
|
1f1346c71f | ||
|
|
b9764a72fb | ||
|
|
f456168428 | ||
|
|
c1b8b06f1a | ||
|
|
fc4298c0f7 | ||
|
|
3c70a893cd | ||
|
|
4ec16ac78b | ||
|
|
b3be17c11e | ||
|
|
2e2c8b41da | ||
|
|
ca0b4820e7 | ||
|
|
ce387117dc | ||
|
|
eaee2cf3b6 | ||
|
|
6bafb1e0a3 | ||
|
|
28bd57bbb4 | ||
|
|
9096026bd2 | ||
|
|
aa7cd5e545 | ||
|
|
840fd161db | ||
|
|
5856b35b1c | ||
|
|
ae77782239 | ||
|
|
1cb982d2bc | ||
|
|
85f450578a | ||
|
|
7d62ba2cf7 | ||
|
|
219ac24f7c | ||
|
|
cfb3e217f5 | ||
|
|
1d06b081b5 | ||
|
|
94ca7d3771 | ||
|
|
2c424d9e76 | ||
|
|
a5a801e18a | ||
|
|
494ec25b13 | ||
|
|
4ad6e9a5c5 | ||
|
|
3746aae577 | ||
|
|
357d323882 | ||
|
|
f0cd913c49 | ||
|
|
1f514824d2 | ||
|
|
2da1a53500 | ||
|
|
a53dce6b3a | ||
|
|
4bc5c8645a | ||
|
|
8337295eb7 | ||
|
|
6e1819b8dc | ||
|
|
a03d4db0fb | ||
|
|
eefa7f56ca | ||
|
|
42e1cf573d | ||
|
|
86a604132d | ||
|
|
843b8692f1 | ||
|
|
877043a805 | ||
|
|
573ba1217b | ||
|
|
4b786f0c90 | ||
|
|
ea78adff35 | ||
|
|
be5feac659 | ||
|
|
a2cd8afc13 | ||
|
|
73d80e123c | ||
|
|
07fe459835 | ||
|
|
40eecd94a5 | ||
|
|
77e7015434 | ||
|
|
97afa3f8cc | ||
|
|
43d1291535 | ||
|
|
a5adb443aa | ||
|
|
57835ea028 | ||
|
|
77c90a977b | ||
|
|
3ac7b1f3e1 | ||
|
|
29f75a4520 | ||
|
|
77c7c3995f | ||
|
|
90a23fc0b0 | ||
|
|
19f80ebd1f | ||
|
|
18e357e19e | ||
|
|
c9c58e81da | ||
|
|
9aae99e226 | ||
|
|
7b04118280 | ||
|
|
37caa70670 | ||
|
|
36889e13ef | ||
|
|
588892a668 | ||
|
|
09428cac3b | ||
|
|
0cecec8440 | ||
|
|
32248ee490 | ||
|
|
d93b8a4ae2 | ||
|
|
88a50e61e4 | ||
|
|
481f526b86 | ||
|
|
4057909f9b | ||
|
|
6f6a0d5f59 | ||
|
|
0d4dc1f921 | ||
|
|
77c2e1237b | ||
|
|
2a3dfe6403 | ||
|
|
45d4637caa | ||
|
|
d4f1de07fe | ||
|
|
42c332f433 | ||
|
|
93ff9217bd | ||
|
|
03074647e9 | ||
|
|
0c6bae9a90 | ||
|
|
78c48adfbf | ||
|
|
1152c7feca | ||
|
|
728ef587e1 | ||
|
|
9393c20ea8 | ||
|
|
20319d0b8e | ||
|
|
28e7d0c0fd | ||
|
|
1e36c3268a | ||
|
|
80e2c37751 | ||
|
|
32a4c1e72e | ||
|
|
8f8004d4cf | ||
|
|
63db18d228 | ||
|
|
bb57c215aa | ||
|
|
58ac84bdc3 | ||
|
|
f8f89d236c | ||
|
|
afbdce3d5f | ||
|
|
a7980e6941 | ||
|
|
7f3e4171a3 | ||
|
|
7157493327 | ||
|
|
238ef75623 | ||
|
|
6afe0bc42e | ||
|
|
145e7986a2 | ||
|
|
d5b24ddb68 | ||
|
|
c2c8be5a6e | ||
|
|
6bba7dbe9d | ||
|
|
76066186dd | ||
|
|
fd594bddf9 | ||
|
|
cf5820f73b | ||
|
|
e6c631826b | ||
|
|
c707081985 | ||
|
|
c7737b6ebf | ||
|
|
8534acb3ee | ||
|
|
115bbe63d8 | ||
|
|
ad2f836451 | ||
|
|
e60009d372 | ||
|
|
197b7eb725 | ||
|
|
940af56a2b | ||
|
|
ea20d3e518 | ||
|
|
837836ab79 | ||
|
|
bc104c49af | ||
|
|
ed6f660099 | ||
|
|
742b762ae2 | ||
|
|
61e8a956c9 | ||
|
|
83c3949b31 | ||
|
|
719de2891e | ||
|
|
df4e5e6732 | ||
|
|
e770c12230 | ||
|
|
8826b699b2 | ||
|
|
fbd888bb99 | ||
|
|
850bd2e1a6 | ||
|
|
52390dc14d | ||
|
|
bc95a0a794 | ||
|
|
ed3e70f973 | ||
|
|
75845d49f9 | ||
|
|
2ada19f449 | ||
|
|
e5a175db47 | ||
|
|
67649d1985 | ||
|
|
030c056aed | ||
|
|
f7bbf04070 | ||
|
|
ae8d9de411 | ||
|
|
2ddfdb24dd | ||
|
|
01b89541c0 | ||
|
|
0c4e21dd50 | ||
|
|
bc73f4ebfa | ||
|
|
f48b7044ac | ||
|
|
ccff5d3f6a | ||
|
|
080cf484f4 | ||
|
|
74fd732c40 | ||
|
|
e5cfb6042a | ||
|
|
13f77d9a90 | ||
|
|
2a259d9cfd | ||
|
|
9c5cd2a9b6 | ||
|
|
4063d82abb | ||
|
|
c09722ac76 | ||
|
|
9727d34afb | ||
|
|
ddaa1669d0 | ||
|
|
3424ea1193 | ||
|
|
38dd2eb250 | ||
|
|
8e4a2e72b4 | ||
|
|
e273d27b30 | ||
|
|
3593059c01 | ||
|
|
bf64f56f7c | ||
|
|
7178983a36 | ||
|
|
187c008e41 | ||
|
|
1a10721dd1 | ||
|
|
2a8fbd515f | ||
|
|
85b9082bec | ||
|
|
e48e875341 | ||
|
|
31f446c96e | ||
|
|
4436cdb037 | ||
|
|
81716c19fa | ||
|
|
6b185fb9a8 | ||
|
|
9367f340d7 | ||
|
|
1177d6d8aa | ||
|
|
89ffc60d7a | ||
|
|
065fcb6c53 | ||
|
|
7c3206bb32 | ||
|
|
34e315cc47 | ||
|
|
ca52f626a8 | ||
|
|
d26c7e3f3c | ||
|
|
8e6fe09971 | ||
|
|
793983907a | ||
|
|
69b37335de | ||
|
|
c71d87a70c | ||
|
|
f1f62b9932 | ||
|
|
c6298c6c15 | ||
|
|
a9af955c71 | ||
|
|
cdadbe9e3b | ||
|
|
24329b3bba | ||
|
|
7fb5def05b | ||
|
|
5a2a1b864c | ||
|
|
ab30e7306b | ||
|
|
5fccbfdde1 | ||
|
|
9af839c317 | ||
|
|
f7c2ed0c01 | ||
|
|
a28c694cdb | ||
|
|
76cce3fbd2 | ||
|
|
5495b5c0a9 | ||
|
|
4fdb1e5ecc | ||
|
|
a4ba74efed | ||
|
|
9883f28717 | ||
|
|
4f7ad214ad | ||
|
|
56838348c5 | ||
|
|
835e708ccd | ||
|
|
ccbe778908 | ||
|
|
3c73f03b78 | ||
|
|
2250a86ef9 | ||
|
|
9e579f0444 | ||
|
|
0fbad3cfda | ||
|
|
10e7a3ce63 | ||
|
|
a6597af965 | ||
|
|
fe2cac9632 | ||
|
|
c9740191e1 | ||
|
|
2f711195a4 | ||
|
|
cb54e705ee | ||
|
|
dfcd509b96 | ||
|
|
09e35028ec | ||
|
|
c48d66768b | ||
|
|
eb0da6e71e | ||
|
|
28995a6ad3 | ||
|
|
258fc9dc50 | ||
|
|
49d777842b | ||
|
|
4078809c13 | ||
|
|
d46b1b1749 | ||
|
|
c97841701a | ||
|
|
8b5b6fe18a | ||
|
|
9c900c6484 | ||
|
|
3f2f3fdfa3 | ||
|
|
81b88e0846 | ||
|
|
9dc169a8a6 | ||
|
|
63443ab1cc | ||
|
|
287c9a1b18 | ||
|
|
a03da9c10b | ||
|
|
a33c4a97c7 | ||
|
|
0116c25ddd | ||
|
|
df38977b65 | ||
|
|
0ade541501 | ||
|
|
de3ef5a93f | ||
|
|
9fa4d6d2d8 | ||
|
|
9f27af17fb | ||
|
|
3ab5889e73 | ||
|
|
4812b9e602 | ||
|
|
47c2d94154 | ||
|
|
b2a1869089 | ||
|
|
15cea9bc3d | ||
|
|
bb05575437 | ||
|
|
428e7ae00f | ||
|
|
0e383ccc0d | ||
|
|
a7ba47d15b | ||
|
|
4747dea06b | ||
|
|
0f9393d3e6 | ||
|
|
f7d8104531 | ||
|
|
7fdc8d322a | ||
|
|
ac05778757 | ||
|
|
ea7343513d | ||
|
|
ed1b1a2f6d | ||
|
|
5aac518cec | ||
|
|
bf1d1660e4 | ||
|
|
8e993b4f93 | ||
|
|
55e9e74294 | ||
|
|
20b5c0b593 | ||
|
|
78ce68b30b | ||
|
|
de319c038c | ||
|
|
ef4764b3d4 | ||
|
|
248293ed97 | ||
|
|
dc2df92cc0 | ||
|
|
303aa60624 | ||
|
|
085377cca9 | ||
|
|
7670a07ca2 | ||
|
|
813e05e9c2 | ||
|
|
c1f470f73f | ||
|
|
2c98737e21 | ||
|
|
8e7571adbc | ||
|
|
0b3802fcd6 | ||
|
|
143c93364f |
11
.gitignore
vendored
@@ -2,17 +2,26 @@
|
||||
node_modules/
|
||||
.history/
|
||||
.vscode/
|
||||
build/
|
||||
extensions/
|
||||
.idea/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
logs/
|
||||
|
||||
# Files
|
||||
package-lock.json
|
||||
.stylelintcache
|
||||
yarn-error.log
|
||||
pnpm-lock.yaml
|
||||
.eslintcache
|
||||
stats.json
|
||||
yarn.lock
|
||||
keys.json
|
||||
.DS_Store
|
||||
*.zip
|
||||
i18n-fallback.json
|
||||
bun.lockb
|
||||
.env
|
||||
*.log
|
||||
*.tmp
|
||||
129
README.md
@@ -1,121 +1,50 @@
|
||||
<img src="assets/logo.png" align="left" width="180px" height="180px"/>
|
||||
<img align="left" width="0" height="192px" hspace="10"/>
|
||||

|
||||
|
||||
> <a href="https://muetab.com/">Mue</a>
|
||||
<h1 align="center">Mue</h1>
|
||||
|
||||
[](/LICENSE) [](https://discord.gg/zv8C9F8) []()
|
||||
<br>
|
||||
[](https://microsoftedge.microsoft.com/addons/detail/aepnglgjfokepefimhbnibfjekidhmja) [](https://addons.mozilla.org/firefox/addon/mue) [](https://chrome.google.com/webstore/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
<p align="center">A fast, open and free-to-use browser extension that gives a new, fresh and customisable tab page to modern browsers.</p>
|
||||
|
||||
Mue is a fast, open and free-to-use browser extension that gives a new, fresh and customisable tab page to modern browsers.
|
||||
<p align="center"><i>Managed by <a href="https://kaiso.one" target="_blank">Kaiso</a> and maintained by contributors all over the world.</i></p>
|
||||
|
||||
<br>
|
||||
<p align="center"><a href="https://muetab.com">muetab.com</a></p>
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Features](#features)
|
||||
- [Planned Features](#planned-features)
|
||||
- [Installation](#installation)
|
||||
- [Chrome](#chrome)
|
||||
- [Firefox](#firefox)
|
||||
- [Edge (Chromium)](#edge-chromium)
|
||||
- [Whale](#whale)
|
||||
- [Other](#other)
|
||||
- [Development](#development)
|
||||
- [Translations](#translations)
|
||||
- [Credits](#credits)
|
||||
- [Developers](#developers)
|
||||
- [Translators](#translators)
|
||||
- [Contributors](#contributors)
|
||||
- [Resources](#resources)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Fast and free
|
||||
- Supports multiple browsers
|
||||
- Actively developed and open source
|
||||
- Automatically updating [API](https://github.com/mue/api) with new photos, quotes and offline mode
|
||||
- Widgets such as search bar, weather, quick links, clock, date, quote, greeting
|
||||
- Settings - enable/disable various features and customise parts of Mue
|
||||
- Navbar with copy button, favourite background, notes feature etc
|
||||
- [Marketplace](https://github.com/mue/marketplace) - download custom photo packs, quote packs and preset settings made by the community
|
||||
|
||||
### Planned Features
|
||||
|
||||
Please see our [roadmap](https://trello.com/b/w7zhS7Hi/mue-50). We are currently working on a rewrite over on the "phoenix" branch.
|
||||
## Why Mue?
|
||||
- Beautiful and Minimalist Design
|
||||
- Customisable Layout
|
||||
- Widgets (such as weather, notes, bookmarks and more)
|
||||
- Privacy-Focused - does not track your browsing activity
|
||||
- Extensible with the Mue Marketplace
|
||||
- Open Source under the BSD-3 License
|
||||
|
||||
## Installation
|
||||
Mue can be downloaded on the following browsers:
|
||||
|
||||
_A demo of the tab can be found [here](https://demo.muetab.com), and the latest GitHub commit build [here](https://mue.vercel.app)_
|
||||
- [Chrome](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
- [Edge](https://microsoftedge.microsoft.com/addons/detail/mue/aepnglgjfokepefimhbnibfjekidhmja)
|
||||
- [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/mue/)
|
||||
- [Whale](https://store.whale.naver.com/detail/ecllekeilcmicbfkkiknfdddbogibbnc)
|
||||
|
||||
### Chrome
|
||||
and can be manually sideloaded on others using the files on [GitHub Releases](https://github.com/mue/mue/releases)
|
||||
|
||||
[](https://chrome.google.com/webstore/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
<br>
|
||||
[Chrome Web Store](https://chrome.google.com/webstore/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
|
||||
### Firefox
|
||||
## Demo
|
||||
A fully-featured demo of the tab extension is available in-browser at [demo.muetab.com](https://demo.muetab.com)
|
||||
|
||||
[](https://addons.mozilla.org/firefox/addon/mue)
|
||||
<br>
|
||||
[Firefox Add-ons](https://addons.mozilla.org/firefox/addon/mue)
|
||||
|
||||
### Edge (Chromium)
|
||||
|
||||
[Microsoft Edge Addons](https://microsoftedge.microsoft.com/addons/detail/aepnglgjfokepefimhbnibfjekidhmja)
|
||||
|
||||
### Whale
|
||||
|
||||
[Whale Store](https://store.whale.naver.com/detail/ecllekeilcmicbfkkiknfdddbogibbnc)
|
||||
|
||||
### Other
|
||||
|
||||
[GitHub Releases](https://github.com/mue/mue/releases)
|
||||
|
||||
## Development
|
||||
Install dependencies with ``bun install``, and then you can run any of the following scripts as needed:
|
||||
|
||||
Please see the [documentation](https://docs.muetab.com/development#mue-tab).
|
||||
- `bun run dev[:host]` - start development server
|
||||
- `bun run build` - build production copy of Mue
|
||||
- `bun run lint[:fix]` - run linter
|
||||
- `bun run pretty` - run prettier
|
||||
- `bun run translations` - migrate old translation format to new
|
||||
|
||||
### Translations
|
||||
|
||||
[](https://hosted.weblate.org/engage/mue/)
|
||||
|
||||
## Credits
|
||||
|
||||
### Developers
|
||||
|
||||
[David Ralph](https://github.com/davidcralph) - Lead development, photographer <br/>
|
||||
[Alex Sparkes](https://github.com/alexsparkes) - Name, lead design, photographer <br/>
|
||||
[Isaac Saunders](https://github.com/eartharoid) - QA, development, photographer <br/>
|
||||
[Wessel Tip](https://github.com/Wessel) - Development <br/>
|
||||
|
||||
### Translators
|
||||
|
||||
[Wessel Tip](https://github.com/Wessel), [Heimen Stoffels](https://github.com/Vistaus) - Dutch <br/>
|
||||
[Alex Sparkes](https://github.com/alexsparkes), [Maxime](https://github.com/exiam) - French <br/>
|
||||
[Anders](https://github.com/FuryingFox) - Norwegian <br/>
|
||||
[Pronin Egor](https://github.com/MrZillaGold) - Russian <br/>
|
||||
[Vicente](https://github.com/Vicente015) - Spanish <br/>
|
||||
[Austin Huang](https://github.com/austinhuang0131) - Chinese (Simplified) <br/>
|
||||
[FreeFun](https://github.com/xXFreeFunXx) - German <br/>
|
||||
[Aksal](https://github.com/aksalsf) - Indonesian <br/>
|
||||
[Kağan Can Şit](https://github.com/KaganCanSit) - Turkish <br/>
|
||||
efeaydal - Turkish <br/>
|
||||
|
||||
### Contributors
|
||||
|
||||
Many thanks to the photographers [here](https://api.muetab.com/images/photographers) for letting us use their wonderful photographs.
|
||||
|
||||
And finally, a big thank you to all the other [contributors](https://github.com/mue/mue/graphs/contributors)!
|
||||
|
||||
### Resources
|
||||
## Translations
|
||||
We use [Weblate](https://weblate.org) for translations. To get started, please visit the [project](https://hosted.weblate.org/projects/mue/) and look for the latest version to start translating Mue into your langauge.
|
||||
|
||||
## Attribution
|
||||
[Pexels](https://pexels.com), [Unsplash](https://unsplash.com) - Stock photos used for offline mode <br/>
|
||||
[Undraw](https://undraw.co) - Welcome modal images
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 222 KiB |
BIN
assets/mue_readme.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 76 KiB |
61
build/finalise.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from `node:fs`;
|
||||
import ADMZip from 'adm-zip';
|
||||
import * as pkg from '../package.json';
|
||||
|
||||
export default function finalise(isProd) {
|
||||
return {
|
||||
name: 'finalise',
|
||||
writeBundle() {
|
||||
if (isProd) {
|
||||
// clean up
|
||||
if (fs.existsSync('./extensions')) {
|
||||
fs.rmSync('./extensions', { recursive: true });
|
||||
}
|
||||
|
||||
// prettify and move i18n report
|
||||
fs.writeFileSync(
|
||||
'./i18n-fallback.json',
|
||||
JSON.stringify(
|
||||
JSON.parse(fs.readFileSync('./dist/i18n-fallback.json', 'utf8')),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
fs.rmSync('./dist/i18n-fallback.json');
|
||||
|
||||
for (const browser of ['chrome', 'firefox']) {
|
||||
// set up
|
||||
fs.mkdirSync(`./extensions/${browser}`, { recursive: true });
|
||||
// copy manifest
|
||||
fs.copyFileSync(
|
||||
`./manifest/${browser}.json`,
|
||||
`./extensions/${browser}/manifest.json`,
|
||||
);
|
||||
// copy service worker
|
||||
fs.copyFileSync(
|
||||
'./manifest/background.js',
|
||||
`./extensions/${browser}/background.js`,
|
||||
);
|
||||
// chrome is weird
|
||||
if (browser === `${browser}`) {
|
||||
fs.cpSync(
|
||||
'./manifest/_locales',
|
||||
`./extensions/${browser}/_locales`,
|
||||
{
|
||||
recursive: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
// copy build
|
||||
fs.cpSync('./dist', `./extensions/${browser}/`, {
|
||||
recursive: true,
|
||||
});
|
||||
// package
|
||||
const cZip = new ADMZip();
|
||||
cZip.addLocalFolder(`./extensions/${browser}`);
|
||||
cZip.writeZip(`./extensions/${browser}-${pkg.version}.zip`);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
1
build/i18n.js
Normal file
@@ -0,0 +1 @@
|
||||
// https://hosted.weblate.org/api/components/mue/mue-tab/statistics/
|
||||
16
cypress.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
5
cypress/e2e/appearance.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/date.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/greeting.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/marketplace.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/message.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/navbar.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/quicklinks.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/quote.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/search.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/stats.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/time.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
5
cypress/e2e/weather.cy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('template spec', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
})
|
||||
200
cypress/e2e/welcome.cy.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/* eslint-disable no-undef */
|
||||
describe('initial modal open', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeContent').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('discord link', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeNotice a').eq(0).invoke('removeAttr', 'target').click()
|
||||
cy.url().then((url) => {
|
||||
expect(url).to.include('discord.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('contribute link', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeNotice a').eq(1).invoke('removeAttr', 'target').click()
|
||||
cy.url().then(url => {
|
||||
expect(url).to.include('github.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('preview function enable/disable', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
// does the preview button exist?
|
||||
cy.get('.welcomeButtons button').eq(0).click()
|
||||
|
||||
// did preview load correctly?
|
||||
cy.get('.preview-mode').should('be.visible')
|
||||
|
||||
// go back
|
||||
cy.get('.preview-mode button').click()
|
||||
|
||||
// are we back?
|
||||
cy.get('.welcomeContent').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('second tab navigation', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
|
||||
// are we on the second tab?
|
||||
cy.get('.languageSettings').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('change to each language setting and back', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
|
||||
// change to each language setting
|
||||
cy.get('.languageSettings span').each(($el, index, $list) => {
|
||||
// press the next one
|
||||
cy.get('.languageSettings span').eq(index).click()
|
||||
// is it checked? state=checked
|
||||
cy.get('.languageSettings span').eq(index).should('have.attr', 'state', 'checked')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('third tab navigation', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
// go to second
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
|
||||
// go to third
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
})
|
||||
})
|
||||
|
||||
describe('import settings', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fourth tab navigation', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
})
|
||||
})
|
||||
|
||||
describe('theme change', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('fifth tab navigation', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
})
|
||||
})
|
||||
|
||||
describe('style choice', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('sixth tab navigation', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
})
|
||||
})
|
||||
|
||||
describe('offline mode check', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('privacy policy link', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeNotice a').eq(2).invoke('removeAttr', 'target').click()
|
||||
cy.url().then(url => {
|
||||
expect(url).to.include('github.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('source code link', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeNotice a').eq(2).invoke('removeAttr', 'target').click()
|
||||
cy.url().then(url => {
|
||||
expect(url).to.include('github.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('final tab navigation', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
})
|
||||
})
|
||||
|
||||
// describe the changes list
|
||||
|
||||
describe('finish button', () => {
|
||||
it('passes', () => {
|
||||
cy.visit('http://localhost:5173')
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(1).click()
|
||||
cy.get('.welcomeButtons button').eq(2).click()
|
||||
})
|
||||
})
|
||||
5
cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
25
cypress/support/commands.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
12
cypress/support/component-index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
24
cypress/support/component.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
import { mount } from 'cypress/react18'
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
17
cypress/support/e2e.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/src/assets/icons/32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/src/assets/icons/16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/16x16.png" />
|
||||
<title>New Tab</title>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"default_locale": "en",
|
||||
"name": "__MSG_name__",
|
||||
"description": "__MSG_description__",
|
||||
"version": "7.1.2",
|
||||
"version": "8.0.0",
|
||||
"homepage_url": "https://muetab.com",
|
||||
"action": {
|
||||
"default_icon": "icons/128x128.png"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Mue",
|
||||
"description": "Fast, open and free-to-use new tab page for modern browsers.",
|
||||
"version": "7.1.2",
|
||||
"version": "8.0.0",
|
||||
"homepage_url": "https://muetab.com",
|
||||
"action": {
|
||||
"default_icon": "icons/128x128.png"
|
||||
@@ -17,5 +17,10 @@
|
||||
},
|
||||
"chrome_settings_overrides": {
|
||||
"homepage": "index.html"
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{ac143a20-4b61-4c81-abdd-4bff77032972}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
migrate-translations.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const fs = require('fs');
|
||||
const YAML = require('yaml');
|
||||
|
||||
const compareAndRemoveKeys = (json1, json2) => {
|
||||
for (let key in json1) {
|
||||
if (json2.hasOwnProperty(key)) {
|
||||
if (typeof json1[key] === 'object' && typeof json2[key] === 'object') {
|
||||
compareAndRemoveKeys(json1[key], json2[key]);
|
||||
} else {
|
||||
if (json1[key] === json2[key]) {
|
||||
delete json1[key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete json1[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const original = JSON.parse(fs.readFileSync(`./src/i18n/locales/en-GB.json`, 'utf8'));
|
||||
|
||||
fs.readdirSync('./src/i18n/locales').forEach((e) => {
|
||||
if (!e.endsWith('json')) return;
|
||||
const data = JSON.parse(fs.readFileSync(`./src/i18n/locales/${e}`, 'utf8'));
|
||||
const name = e.replace('.json', '');
|
||||
|
||||
if (name !== 'en-GB') {
|
||||
compareAndRemoveKeys(data, original);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(`./src/i18n/${name}`);
|
||||
} catch (e) {}
|
||||
|
||||
const _addons = YAML.stringify(data.modals?.main?.addons) || '{}';
|
||||
fs.writeFileSync(`./src/i18n/${name}/_addons.yml`, _addons);
|
||||
delete data?.modals?.main?.addons;
|
||||
|
||||
const _marketplace = YAML.stringify(data.modals?.main?.marketplace) || '{}';
|
||||
fs.writeFileSync(`./src/i18n/${name}/_marketplace.yml`, _marketplace);
|
||||
delete data?.modals?.main?.marketplace;
|
||||
|
||||
const _settings = YAML.stringify(data.modals?.main?.settings) || '{}';
|
||||
fs.writeFileSync(`./src/i18n/${name}/_settings.yml`, _settings);
|
||||
delete data?.modals?.main?.settings;
|
||||
|
||||
const _welcome = YAML.stringify(data.modals?.welcome) || '{}';
|
||||
fs.writeFileSync(`./src/i18n/${name}/_welcome.yml`, _welcome);
|
||||
delete data?.modals?.welcome;
|
||||
|
||||
const main = YAML.stringify(data) || '{}';
|
||||
fs.writeFileSync(`./src/i18n/${name}/main.yml`, main);
|
||||
});
|
||||
|
||||
fs.readdirSync('./src/i18n/locales/achievements').forEach((e) => {
|
||||
if (!e.endsWith('json')) return;
|
||||
const data = JSON.parse(fs.readFileSync(`./src/i18n/locales/achievements/${e}`, 'utf8'));
|
||||
const name = e.replace('.json', '');
|
||||
|
||||
if (name !== 'en-GB') {
|
||||
compareAndRemoveKeys(data, original);
|
||||
}
|
||||
|
||||
const _achievements = YAML.stringify(data) || '{}';
|
||||
fs.writeFileSync(`./src/i18n/${name}/_achievements.yml`, _achievements);
|
||||
});
|
||||
61
package.json
@@ -9,56 +9,69 @@
|
||||
"homepage": "https://muetab.com",
|
||||
"bugs": "https://github.com/mue/mue/issues/new?assignees=&labels=bug&template=bug-report.md&title=%5BBUG%5D",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "7.1.2",
|
||||
"version": "8.0.0",
|
||||
"dependencies": {
|
||||
"@eartharoid/i18n": "1.2.1",
|
||||
"@eartharoid/i18n": "2.0.0-alpha.1",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@floating-ui/react-dom": "2.1.1",
|
||||
"@fontsource/lexend-deca": "5.0.14",
|
||||
"@fontsource/montserrat": "5.0.19",
|
||||
"@floating-ui/react-dom": "2.1.0",
|
||||
"@fontsource-variable/lexend-deca": "^5.1.0",
|
||||
"@fontsource-variable/montserrat": "^5.1.0",
|
||||
"@headlessui/react": "^2.1.9",
|
||||
"@muetab/react-sortable-hoc": "^2.0.1",
|
||||
"@mui/material": "6.0.2",
|
||||
"@sentry/react": "^8.28.0",
|
||||
"embla-carousel-autoplay": "8.2.1",
|
||||
"embla-carousel-react": "8.2.1",
|
||||
"@mui/material": "5.15.19",
|
||||
"@sentry/react": "^8.33.1",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-autoplay": "8.1.3",
|
||||
"embla-carousel-react": "8.1.3",
|
||||
"fast-blurhash": "^1.1.4",
|
||||
"image-conversion": "^2.1.1",
|
||||
"framer-motion": "^11.11.0",
|
||||
"markdown-to-jsx": "^7.5.0",
|
||||
"mue": "file:",
|
||||
"react": "^18.3.1",
|
||||
"react-best-gradient-color-picker": "^3.0.10",
|
||||
"react-clock": "5.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-modal": "3.16.1",
|
||||
"react-slider": "^2.0.6",
|
||||
"react-toastify": "10.0.5",
|
||||
"recharts": "^2.13.3",
|
||||
"use-debounce": "^10.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.4.1",
|
||||
"@commitlint/config-conventional": "^19.4.1",
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@eartharoid/deep-merge": "^0.0.2",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"adm-zip": "0.5.16",
|
||||
"eslint": "^8.57.0",
|
||||
"@eartharoid/vite-plugin-i18n": "1.0.0-alpha.7",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@vitejs/plugin-react-swc": "^3.7.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cypress": "^13.17.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"husky": "^9.1.5",
|
||||
"husky": "^9.1.6",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.78.0",
|
||||
"sass": "^1.79.4",
|
||||
"stylelint": "^16.9.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"stylelint-scss": "^6.5.1",
|
||||
"vite": "5.4.3",
|
||||
"vite-plugin-progress": "^0.0.7"
|
||||
"stylelint-scss": "^6.7.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"vite": "5.2.12",
|
||||
"vite-plugin-inspect": "^0.8.7",
|
||||
"vite-plugin-progress": "^0.0.7",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"cy:open": "cypress open",
|
||||
"dev": "vite",
|
||||
"dev:host": "vite --host",
|
||||
"translations": "cd scripts && node updatetranslations.js",
|
||||
"build": "vite build",
|
||||
"pretty": "prettier --write \"./**/*.{js,jsx,json,scss,css}\"",
|
||||
"lint": "eslint \"./src/**/*.{js,jsx}\" && stylelint \"./src/**/*.{scss,css}\"",
|
||||
"lint:fix": "eslint \"./src/**/*.{js,jsx}\" --fix && stylelint \"./src/**/*.{scss,css}\" --fix",
|
||||
"postinstall": "husky"
|
||||
"postinstall": "husky",
|
||||
"pretty": "prettier --write \"./**/*.{js,jsx,json,scss,css}\""
|
||||
}
|
||||
}
|
||||
|
||||
6548
pnpm-lock.yaml
generated
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 804 B After Width: | Height: | Size: 804 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
BIN
public/theme-examples/dark.jpg
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
public/theme-examples/light.jpg
Normal file
|
After Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
@@ -1,85 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const merge = require('@eartharoid/deep-merge');
|
||||
|
||||
const compareAndRemoveKeys = (json1, json2) => {
|
||||
for (let key in json1) {
|
||||
if (json2.hasOwnProperty(key)) {
|
||||
if (typeof json1[key] === 'object' && typeof json2[key] === 'object') {
|
||||
compareAndRemoveKeys(json1[key], json2[key]);
|
||||
}
|
||||
} else {
|
||||
delete json1[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const localesDir = path.join(__dirname, '../src/i18n/locales');
|
||||
const achievementsDir = path.join(localesDir, 'achievements');
|
||||
|
||||
// Check if the locales directory exists, if not, create it
|
||||
if (!fs.existsSync(localesDir)) {
|
||||
fs.mkdirSync(localesDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Check if the achievements directory exists, if not, create it
|
||||
if (!fs.existsSync(achievementsDir)) {
|
||||
fs.mkdirSync(achievementsDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.readdirSync(localesDir).forEach((file) => {
|
||||
if (file === 'en_GB.json') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.lstatSync(path.join(localesDir, file)).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const en = require(path.join(localesDir, 'en_GB.json'));
|
||||
const newdata = merge(en, require(path.join(localesDir, file)));
|
||||
|
||||
compareAndRemoveKeys(newdata, en);
|
||||
|
||||
fs.writeFileSync(path.join(localesDir, file), JSON.stringify(newdata, null, 2));
|
||||
|
||||
fs.appendFileSync(path.join(localesDir, file), '\n');
|
||||
});
|
||||
|
||||
fs.readdirSync(achievementsDir).forEach((file) => {
|
||||
if (file === 'en_GB.json') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.lstatSync(path.join(achievementsDir, file)).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enGBFilePath = path.join(achievementsDir, 'en_GB.json');
|
||||
if (!fs.existsSync(enGBFilePath)) {
|
||||
console.error(`File 'en_GB.json' does not exist in the directory '${achievementsDir}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const en = require(enGBFilePath);
|
||||
const newdata = merge(en, require(path.join(achievementsDir, file)));
|
||||
|
||||
compareAndRemoveKeys(newdata, en);
|
||||
|
||||
fs.writeFileSync(path.join(achievementsDir, file), JSON.stringify(newdata, null, 2));
|
||||
|
||||
fs.appendFileSync(path.join(achievementsDir, file), '\n');
|
||||
|
||||
const locales = fs.readdirSync(localesDir);
|
||||
locales.forEach((locale) => {
|
||||
if (!fs.existsSync(path.join(achievementsDir, locale))) {
|
||||
// ignore directories
|
||||
if (fs.lstatSync(path.join(localesDir, locale)).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(achievementsDir, locale), JSON.stringify(en, null, 2));
|
||||
fs.appendFileSync(path.join(achievementsDir, locale), '\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
40
src/App.jsx
@@ -3,20 +3,22 @@ import { ToastContainer } from 'react-toastify';
|
||||
import Background from 'features/background/Background';
|
||||
import Widgets from 'features/misc/views/Widgets';
|
||||
import Modals from 'features/misc/modals/Modals';
|
||||
import { loadSettings, moveSettings } from 'utils/settings';
|
||||
import { loadSettings } from 'utils/settings';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import variables from 'config/variables';
|
||||
import Preview from 'features/helpers/preview/Preview';
|
||||
import Stats from 'features/stats/api/stats';
|
||||
|
||||
import Welcome from 'features/welcome/Welcome';
|
||||
|
||||
import BackgroundDefaults from 'features/background/options/default';
|
||||
import defaults from 'config/default';
|
||||
|
||||
import '@fontsource-variable/lexend-deca';
|
||||
import '@fontsource-variable/montserrat';
|
||||
|
||||
const useAppSetup = () => {
|
||||
useEffect(() => {
|
||||
const firstRun = localStorage.getItem('firstRun');
|
||||
const stats = localStorage.getItem('stats');
|
||||
|
||||
if (!firstRun || !stats) {
|
||||
moveSettings();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
|
||||
const refreshHandler = (data) => {
|
||||
@@ -27,8 +29,6 @@ const useAppSetup = () => {
|
||||
|
||||
EventBus.on('refresh', refreshHandler);
|
||||
|
||||
variables.stats.tabLoad();
|
||||
|
||||
return () => {
|
||||
EventBus.off('refresh', refreshHandler);
|
||||
};
|
||||
@@ -40,20 +40,29 @@ const App = () => {
|
||||
const [showBackground, setShowBackground] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const storedToastDisplayTime = localStorage.getItem('toastDisplayTime');
|
||||
const storedBackground = localStorage.getItem('background');
|
||||
const storedToastDisplayTime =
|
||||
localStorage.getItem('toastDisplayTime') || defaults.toastDisplayTime;
|
||||
const storedBackground = localStorage.getItem('background') || BackgroundDefaults.background;
|
||||
|
||||
if (storedToastDisplayTime) {
|
||||
setToastDisplayTime(parseInt(storedToastDisplayTime, 10));
|
||||
}
|
||||
|
||||
if (storedBackground === 'true') {
|
||||
if (storedBackground === 'true' || storedBackground === true) {
|
||||
setShowBackground(true);
|
||||
}
|
||||
|
||||
// Reset tab ID when component mounts and post initial event
|
||||
Stats.generateTabId();
|
||||
Stats.postEvent('new-tab', 'tab', 'opened');
|
||||
}, []);
|
||||
|
||||
useAppSetup();
|
||||
|
||||
if (localStorage.getItem('showWelcome') !== 'false') {
|
||||
return <Welcome />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBackground && <Background />}
|
||||
@@ -68,6 +77,9 @@ const App = () => {
|
||||
<Widgets />
|
||||
<Modals />
|
||||
</div>
|
||||
{localStorage.getItem('welcomePreview') === 'true' && (
|
||||
<Preview setup={() => window.location.reload()} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import variables from 'config/variables';
|
||||
|
||||
import { useState, memo } from 'react';
|
||||
import { TextareaAutosize } from '@mui/material';
|
||||
import { MdAddLink, MdClose } from 'react-icons/md';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import { Button } from 'components/Elements';
|
||||
import { Tooltip, Button } from 'components/Elements';
|
||||
import { TextareaAutosize } from 'components/Form';
|
||||
|
||||
function AddModal({ urlError, iconError, addLink, closeModal, edit, editData, editLink }) {
|
||||
const [name, setName] = useState(edit ? editData.name : '');
|
||||
@@ -19,7 +18,7 @@ function AddModal({ urlError, iconError, addLink, closeModal, edit, editData, ed
|
||||
? variables.getMessage('widgets.quicklinks.edit')
|
||||
: variables.getMessage('widgets.quicklinks.new')}
|
||||
</span>
|
||||
<Tooltip title={variables.getMessage('modals.welcome.buttons.close')}>
|
||||
<Tooltip title={variables.getMessage('welcome:buttons.close')}>
|
||||
<div className="close" onClick={() => closeModal()}>
|
||||
<MdClose />
|
||||
</div>
|
||||
@@ -56,7 +55,7 @@ function AddModal({ urlError, iconError, addLink, closeModal, edit, editData, ed
|
||||
type="settings"
|
||||
onClick={() => editLink(editData, name, url, icon)}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage('modals.main.settings.sections.quicklinks.edit')}
|
||||
label={variables.getMessage('settings:sections.quicklinks.edit')}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -1,64 +1,90 @@
|
||||
import variables from 'config/variables';
|
||||
import { Suspense, lazy, useState, memo } from 'react';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
|
||||
import { memo, Suspense, lazy } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import './scss/index.scss';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import Navbar from './backend/TabNavbar';
|
||||
import { TabProvider, useTab } from './backend/TabContext';
|
||||
import { MarketplaceDataProvider } from 'features/marketplace/api/MarketplaceDataContext';
|
||||
|
||||
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
|
||||
const Addons = lazy(() => import('../../../features/misc/views/Addons'));
|
||||
const Marketplace = lazy(() => import('../../../features/misc/views/Marketplace'));
|
||||
|
||||
const renderLoader = () => (
|
||||
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
||||
<div className="modalSidebar">
|
||||
<span className="mainTitle">Mue</span>
|
||||
</div>
|
||||
<div className="modalTabContent">
|
||||
<div className="emptyItems">
|
||||
<div className="emptyMessage">
|
||||
<div className="loaderHolder">
|
||||
<div id="loader"></div>
|
||||
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full h-[65vh] justify-center items-center">
|
||||
<div className="loaderHolder">
|
||||
<div id="loader"></div>
|
||||
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function MainModal({ modalClose }) {
|
||||
const [currentTab, setCurrentTab] = useState('settings');
|
||||
const MainModalContent = ({ modalClose }) => {
|
||||
const { activeTab, direction } = useTab();
|
||||
|
||||
const changeTab = (type) => {
|
||||
setCurrentTab(type);
|
||||
const variants = {
|
||||
enter: (direction) => ({
|
||||
x: direction > 0 ? '100%' : '-100%',
|
||||
opacity: 0,
|
||||
top: '80px',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
}),
|
||||
center: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
top: '80px',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
exit: (direction) => ({
|
||||
x: direction < 0 ? '100%' : '-100%',
|
||||
opacity: 0,
|
||||
top: '80px',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
}),
|
||||
};
|
||||
|
||||
const renderTab = () => {
|
||||
switch (currentTab) {
|
||||
switch (activeTab) {
|
||||
case 'addons':
|
||||
return <Addons changeTab={changeTab} />;
|
||||
return <Addons />;
|
||||
case 'marketplace':
|
||||
return <Marketplace changeTab={changeTab} />;
|
||||
return <Marketplace />;
|
||||
default:
|
||||
return <Settings changeTab={changeTab} />;
|
||||
return <Settings />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="frame">
|
||||
<Tooltip
|
||||
style={{ position: 'absolute', top: '1rem', right: '1rem' }}
|
||||
title={variables.getMessage('modals.welcome.buttons.close')}
|
||||
key="closeTooltip"
|
||||
>
|
||||
<span className="closeModal" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Suspense fallback={renderLoader()}>{renderTab()}</Suspense>
|
||||
<div className="flex flex-col w-full min-w-full ">
|
||||
<Navbar modalClose={modalClose} />
|
||||
<AnimatePresence initial={false} custom={direction}>
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
custom={direction}
|
||||
variants={variants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{ type: 'tween', duration: 0.8 }}
|
||||
className="flex w-full min-w-full overflow-y-auto"
|
||||
>
|
||||
<Suspense fallback={renderLoader()}>{renderTab()}</Suspense>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const MainModal = ({ modalClose }) => (
|
||||
<TabProvider>
|
||||
<MarketplaceDataProvider>
|
||||
<MainModalContent modalClose={modalClose} />
|
||||
</MarketplaceDataProvider>
|
||||
</TabProvider>
|
||||
);
|
||||
|
||||
const MemoizedMainModal = memo(MainModal);
|
||||
export { MemoizedMainModal as default, MemoizedMainModal as MainModal };
|
||||
|
||||
@@ -21,41 +21,41 @@ import {
|
||||
MdOutlineAssessment as Stats,
|
||||
MdOutlineNewReleases as Changelog,
|
||||
MdInfoOutline as About,
|
||||
MdOutlineExtension as Added,
|
||||
MdSpaceDashboard as Added,
|
||||
MdAddCircleOutline as Create,
|
||||
MdViewAgenda as Overview,
|
||||
MdCollectionsBookmark as Collections,
|
||||
} from 'react-icons/md';
|
||||
|
||||
const iconMapping = {
|
||||
[variables.getMessage('modals.main.marketplace.product.overview')]: <Overview />,
|
||||
[variables.getMessage('marketplace:product.overview')]: <Overview />,
|
||||
[variables.getMessage('modals.main.navbar.settings')]: <Settings />,
|
||||
[variables.getMessage('modals.main.navbar.addons')]: <Addons />,
|
||||
[variables.getMessage('modals.main.navbar.marketplace')]: <Marketplace />,
|
||||
[variables.getMessage('modals.main.settings.sections.appearance.navbar.title')]: <Navbar />,
|
||||
[variables.getMessage('modals.main.settings.sections.greeting.title')]: <Greeting />,
|
||||
[variables.getMessage('modals.main.settings.sections.time.title')]: <Time />,
|
||||
[variables.getMessage('modals.main.settings.sections.quicklinks.title')]: <QuickLinks />,
|
||||
[variables.getMessage('modals.main.settings.sections.quote.title')]: <Quote />,
|
||||
[variables.getMessage('modals.main.settings.sections.date.title')]: <Date />,
|
||||
[variables.getMessage('modals.main.settings.sections.message.title')]: <Message />,
|
||||
[variables.getMessage('modals.main.settings.sections.background.title')]: <Background />,
|
||||
[variables.getMessage('modals.main.settings.sections.search.title')]: <MdSearch />,
|
||||
[variables.getMessage('modals.main.settings.sections.weather.title')]: <Weather />,
|
||||
[variables.getMessage('modals.main.settings.sections.appearance.title')]: <Appearance />,
|
||||
[variables.getMessage('modals.main.settings.sections.language.title')]: <Language />,
|
||||
[variables.getMessage('modals.main.settings.sections.advanced.title')]: <Advanced />,
|
||||
[variables.getMessage('modals.main.settings.sections.stats.title')]: <Stats />,
|
||||
[variables.getMessage('modals.main.settings.sections.experimental.title')]: <Experimental />,
|
||||
[variables.getMessage('modals.main.settings.sections.changelog.title')]: <Changelog />,
|
||||
[variables.getMessage('modals.main.settings.sections.about.title')]: <About />,
|
||||
[variables.getMessage('modals.main.addons.added')]: <Added />,
|
||||
[variables.getMessage('modals.main.addons.create.title')]: <Create />,
|
||||
[variables.getMessage('modals.main.marketplace.all')]: <Addons />,
|
||||
[variables.getMessage('modals.main.marketplace.photo_packs')]: <Background />,
|
||||
[variables.getMessage('modals.main.marketplace.quote_packs')]: <Quote />,
|
||||
[variables.getMessage('modals.main.marketplace.preset_settings')]: <Advanced />,
|
||||
[variables.getMessage('modals.main.marketplace.collections')]: <Collections />,
|
||||
[variables.getMessage('settings:sections.appearance.navbar.title')]: <Navbar />,
|
||||
[variables.getMessage('settings:sections.greeting.title')]: <Greeting />,
|
||||
[variables.getMessage('settings:sections.time.title')]: <Time />,
|
||||
[variables.getMessage('settings:sections.quicklinks.title')]: <QuickLinks />,
|
||||
[variables.getMessage('settings:sections.quote.title')]: <Quote />,
|
||||
[variables.getMessage('settings:sections.date.title')]: <Date />,
|
||||
[variables.getMessage('settings:sections.message.title')]: <Message />,
|
||||
[variables.getMessage('settings:sections.background.title')]: <Background />,
|
||||
[variables.getMessage('settings:sections.search.title')]: <MdSearch />,
|
||||
[variables.getMessage('settings:sections.weather.title')]: <Weather />,
|
||||
[variables.getMessage('settings:sections.appearance.title')]: <Appearance />,
|
||||
[variables.getMessage('settings:sections.language.title')]: <Language />,
|
||||
[variables.getMessage('settings:sections.advanced.title')]: <Advanced />,
|
||||
[variables.getMessage('settings:sections.stats.title')]: <Stats />,
|
||||
[variables.getMessage('settings:sections.experimental.title')]: <Experimental />,
|
||||
[variables.getMessage('settings:sections.changelog.title')]: <Changelog />,
|
||||
[variables.getMessage('settings:sections.about.title')]: <About />,
|
||||
[variables.getMessage('addons:added')]: <Added />,
|
||||
[variables.getMessage('addons:create.title')]: <Create />,
|
||||
[variables.getMessage('marketplace:all')]: <Addons />,
|
||||
[variables.getMessage('marketplace:photo_packs')]: <Background />,
|
||||
[variables.getMessage('marketplace:quote_packs')]: <Quote />,
|
||||
[variables.getMessage('marketplace:preset_settings')]: <Advanced />,
|
||||
[variables.getMessage('marketplace:collections')]: <Collections />,
|
||||
};
|
||||
|
||||
function Tab({ label, currentTab, onClick, navbarTab }) {
|
||||
@@ -71,34 +71,15 @@ function Tab({ label, currentTab, onClick, navbarTab }) {
|
||||
}
|
||||
|
||||
const icon = iconMapping[label];
|
||||
const divider = [
|
||||
variables.getMessage('modals.main.settings.sections.weather.title'),
|
||||
variables.getMessage('modals.main.settings.sections.language.title'),
|
||||
variables.getMessage('modals.main.marketplace.all'),
|
||||
variables.getMessage('modals.main.settings.sections.experimental.title'),
|
||||
].includes(label);
|
||||
|
||||
const mue = [
|
||||
variables.getMessage('modals.main.marketplace.product.overview'),
|
||||
variables.getMessage('modals.main.addons.added'),
|
||||
variables.getMessage('modals.main.marketplace.all'),
|
||||
].includes(label);
|
||||
|
||||
if (
|
||||
label === variables.getMessage('modals.main.settings.sections.experimental.title') &&
|
||||
!isExperimental
|
||||
) {
|
||||
return <hr />;
|
||||
if (label === variables.getMessage('settings:sections.experimental.title') && !isExperimental) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{mue && <span className="mainTitle">Mue</span>}
|
||||
<button className={className} onClick={() => onClick(label)}>
|
||||
{icon} <span>{label}</span>
|
||||
</button>
|
||||
{divider && <hr />}
|
||||
</>
|
||||
<button className={className} onClick={() => onClick(label)}>
|
||||
{icon} <span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
56
src/components/Elements/MainModal/backend/TabContext.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import variables from 'config/variables';
|
||||
|
||||
const TabContext = createContext();
|
||||
|
||||
export const useTab = () => {
|
||||
return useContext(TabContext);
|
||||
};
|
||||
|
||||
export const TabProvider = ({ children }) => {
|
||||
const [activeTab, setActiveTab] = useState('settings');
|
||||
const [subTab, setSubTab] = useState(variables.getMessage('marketplace:product.overview'));
|
||||
const [subSection, setSubSection] = useState('');
|
||||
const [direction, setDirection] = useState(1);
|
||||
|
||||
const changeTab = (tab, subtab = '', section = '') => {
|
||||
const tabs = [
|
||||
{ id: 'settings', label: 'Settings' },
|
||||
{ id: 'addons', label: 'Addons' },
|
||||
{ id: 'marketplace', label: 'Marketplace' },
|
||||
];
|
||||
|
||||
const currentIndex = tabs.findIndex((t) => t.id === activeTab);
|
||||
const newIndex = tabs.findIndex((t) => t.id === tab);
|
||||
|
||||
setDirection(newIndex > currentIndex ? 1 : -1);
|
||||
setSubTab(subtab);
|
||||
if (tab === 'settings' && subtab === '' && section === '') {
|
||||
setSubTab(variables.getMessage('marketplace:product.overview'));
|
||||
}
|
||||
|
||||
setActiveTab(tab);
|
||||
setSubSection(section);
|
||||
};
|
||||
|
||||
const setSection = (type) => {
|
||||
setSubTab(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabContext.Provider
|
||||
value={{
|
||||
activeTab,
|
||||
subTab,
|
||||
direction,
|
||||
subSection,
|
||||
changeTab,
|
||||
setSubTab,
|
||||
setSection,
|
||||
setSubSection,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabContext.Provider>
|
||||
);
|
||||
};
|
||||
275
src/components/Elements/MainModal/backend/TabNavbar.jsx
Normal file
@@ -0,0 +1,275 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
MdSettings,
|
||||
MdOutlineShoppingBasket,
|
||||
MdSpaceDashboard,
|
||||
MdOutlineKeyboardArrowRight,
|
||||
MdClose,
|
||||
MdSearch,
|
||||
} from 'react-icons/md';
|
||||
import { IoMdPricetag } from 'react-icons/io';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useTab } from './TabContext';
|
||||
import { useMarketData } from 'features/marketplace/api/MarketplaceDataContext';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import variables from 'config/variables';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const TabNavbar = ({ modalClose }) => {
|
||||
const { activeTab, subTab, changeTab, subSection, setSubTab, setSubSection } = useTab();
|
||||
const { setSelectedItem, setSelectedCollection, installedItems } = useMarketData();
|
||||
|
||||
const tabs = [
|
||||
{ id: 'settings', label: 'Settings', icon: <MdSettings /> },
|
||||
{ id: 'addons', label: 'Addons', icon: <MdSpaceDashboard /> },
|
||||
{ id: 'marketplace', label: 'Marketplace', icon: <IoMdPricetag /> },
|
||||
];
|
||||
|
||||
const navbarLogo = (
|
||||
<svg
|
||||
width="123"
|
||||
height="123"
|
||||
viewBox="0 0 123 123"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-[40px] h-[40px]"
|
||||
>
|
||||
<g filter="url(#filter0_d_2473_27)">
|
||||
<circle cx="61.5" cy="61.5" r="50.5" fill="url(#paint0_linear_2473_27)" />
|
||||
<path
|
||||
d="M68.2969 43.1796V48.5603H79.9348V60.1055H85.3638V43.1796H68.2969Z"
|
||||
fill="url(#paint1_linear_2473_27)"
|
||||
/>
|
||||
<path
|
||||
d="M72.1542 61.0483H67.344V65.8527H62.9056V61.0483H58.0919V56.6185H62.9056V51.8175H67.344V56.6185H72.1542V61.0483ZM78.6447 49.6043H67.034V45.5766H47.5625V72.0938H82.6836V61.1961H78.6447V49.6043Z"
|
||||
fill="url(#paint2_linear_2473_27)"
|
||||
/>
|
||||
<path
|
||||
d="M46.358 50.518H42.6289V77.0352H77.75V73.3029H46.358V50.518Z"
|
||||
fill="url(#paint3_linear_2473_27)"
|
||||
/>
|
||||
<path
|
||||
d="M41.4205 55.4516H37.6914V81.9688H72.8125V78.2365H41.4205V55.4516Z"
|
||||
fill="url(#paint4_linear_2473_27)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_2473_27"
|
||||
x="0.3"
|
||||
y="0.3"
|
||||
width="122.4"
|
||||
height="122.4"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="5.35" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2473_27" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_2473_27"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_2473_27"
|
||||
x1="104.324"
|
||||
y1="35.24"
|
||||
x2="16.959"
|
||||
y2="88.366"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FF5C25" />
|
||||
<stop offset="0.484375" stopColor="#D21A11" />
|
||||
<stop offset="1" stopColor="#FF456E" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_2473_27"
|
||||
x1="76.8303"
|
||||
y1="60.1055"
|
||||
x2="76.8303"
|
||||
y2="43.1796"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F18D91" />
|
||||
<stop offset="1" stopColor="#FBD3C6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_2473_27"
|
||||
x1="65.123"
|
||||
y1="72.0938"
|
||||
x2="65.123"
|
||||
y2="45.5766"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F18D91" />
|
||||
<stop offset="1" stopColor="#FBD3C6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_2473_27"
|
||||
x1="60.1895"
|
||||
y1="77.0352"
|
||||
x2="60.1895"
|
||||
y2="50.518"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F18D91" />
|
||||
<stop offset="1" stopColor="#FBD3C6" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_2473_27"
|
||||
x1="55.252"
|
||||
y1="81.9688"
|
||||
x2="55.252"
|
||||
y2="55.4516"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F18D91" />
|
||||
<stop offset="1" stopColor="#FBD3C6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-5 p-5 items-center justify-between">
|
||||
<div className="flex flex-row gap-5 items-center">
|
||||
{navbarLogo}
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span
|
||||
onClick={() => {
|
||||
changeTab(activeTab);
|
||||
setSelectedItem(null);
|
||||
setSelectedCollection(null);
|
||||
}}
|
||||
className={clsx(
|
||||
'text-xl capitalize tracking-normal transition-all duration-150 ease-in-out',
|
||||
{
|
||||
'text-neutral-300 cursor-pointer hover:text-neutral-100':
|
||||
subTab !== '' && (activeTab === 'marketplace' || activeTab === 'addons'),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{variables.getMessage(`modals.main.navbar.${activeTab}`)}
|
||||
</span>
|
||||
{subTab !== '' && (
|
||||
<>
|
||||
<MdOutlineKeyboardArrowRight />
|
||||
<span
|
||||
onClick={() => setSubSection('')}
|
||||
className={clsx(
|
||||
'text-xl capitalize tracking-normal transition-all duration-150 ease-in-out',
|
||||
{
|
||||
'dark:text-neutral-300 text-neutral-600 cursor-pointer hover:text-black dark:hover:text-neutral-100':
|
||||
subSection !== '',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{subTab}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{subSection !== '' && (
|
||||
<>
|
||||
<MdOutlineKeyboardArrowRight />
|
||||
<span className="text-xl capitalize tracking-normal">{subSection}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-5">
|
||||
<AnimatePresence>
|
||||
{activeTab === 'marketplace' && subTab === '' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: '-100%' }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: '-100%' }}
|
||||
>
|
||||
<form className="max-w-md mx-auto relative mr-10">
|
||||
<input
|
||||
label={variables.getMessage('widgets.search')}
|
||||
placeholder={variables.getMessage('widgets.search')}
|
||||
name="filter"
|
||||
id="filter"
|
||||
className="h-[40px] block w-full px-4 ps-10 text-sm text-gray-900 border border-[#484848] rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-white/5 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-neutral-100"
|
||||
/>
|
||||
<div className="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<MdSearch />
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="flex space-x-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
changeTab(tab.id);
|
||||
setSelectedItem(null);
|
||||
setSelectedCollection(null);
|
||||
}}
|
||||
className={`${
|
||||
activeTab === tab.id ? '' : 'dark:hover:text-white/70 hover:text-black/70'
|
||||
} dark:text-white text-black transition-all duration-800 ease-in-out flex flex-row gap-2 items-center relative rounded-sm px-3 py-1.5 text-sm outline-sky-400 transition focus-visible:outline-2`}
|
||||
style={{
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
{activeTab === tab.id && (
|
||||
<motion.span
|
||||
layoutId="tabNavbarBubble"
|
||||
className="absolute inset-0 z-10 bg-[#333] mix-blend-lighten rounded-xl"
|
||||
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
|
||||
/>
|
||||
)}
|
||||
{tab.icon}
|
||||
{variables.getMessage(`modals.main.navbar.${tab.id}`)}
|
||||
{tab.id === 'addons' && (
|
||||
<AnimatePresence>
|
||||
<div className="px-3 py-1 dark:bg-[#424242] bg-neutral-300 rounded-lg text-xs">
|
||||
<motion.span
|
||||
key={installedItems.length}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
>
|
||||
{installedItems.length}
|
||||
</motion.span>
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
{tab.id === 'marketplace' && (
|
||||
<span className="px-3 py-1 bg-rose-800 rounded-lg text-xs border border-rose-700 text-white">
|
||||
NEW
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Tooltip
|
||||
style={{ marginLeft: 'auto', justifySelf: 'flex-end' }}
|
||||
title={variables.getMessage('welcome:buttons.close')}
|
||||
key="closeTooltip"
|
||||
>
|
||||
<span className="closeModal" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { TabNavbar as default, TabNavbar };
|
||||
@@ -1,109 +0,0 @@
|
||||
import variables from 'config/variables';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
MdSettings,
|
||||
MdOutlineShoppingBasket,
|
||||
MdOutlineExtension,
|
||||
MdRefresh,
|
||||
MdClose,
|
||||
} from 'react-icons/md';
|
||||
import Tab from './Tab';
|
||||
import { Button } from 'components/Elements';
|
||||
import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary';
|
||||
|
||||
const Tabs = (props) => {
|
||||
const [currentTab, setCurrentTab] = useState(props.children[0].props.label);
|
||||
const [currentName, setCurrentName] = useState(props.children[0].props.name);
|
||||
|
||||
const onClick = (tab, name) => {
|
||||
if (name !== currentName) {
|
||||
variables.stats.postEvent('tab', `Opened ${name}`);
|
||||
}
|
||||
|
||||
setCurrentTab(tab);
|
||||
setCurrentName(name);
|
||||
};
|
||||
|
||||
const hideReminder = () => {
|
||||
localStorage.setItem('showReminder', false);
|
||||
document.querySelector('.reminder-info').style.display = 'none';
|
||||
};
|
||||
|
||||
const navbarButtons = [
|
||||
{
|
||||
tab: 'settings',
|
||||
icon: <MdSettings />,
|
||||
},
|
||||
{
|
||||
tab: 'addons',
|
||||
icon: <MdOutlineExtension />,
|
||||
},
|
||||
{
|
||||
tab: 'marketplace',
|
||||
icon: <MdOutlineShoppingBasket />,
|
||||
},
|
||||
];
|
||||
|
||||
const reminderInfo = (
|
||||
<div
|
||||
className="reminder-info"
|
||||
style={{ display: localStorage.getItem('showReminder') === 'true' ? 'flex' : 'none' }}
|
||||
>
|
||||
<div className="shareHeader">
|
||||
<span className="title">{variables.getMessage('modals.main.settings.reminder.title')}</span>
|
||||
<span className="closeModal" onClick={hideReminder}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</div>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.reminder.message')}
|
||||
</span>
|
||||
<button onClick={() => window.location.reload()}>
|
||||
<MdRefresh />
|
||||
{variables.getMessage('modals.main.error_boundary.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
||||
<div className="modalSidebar">
|
||||
{props.children.map((tab, index) => (
|
||||
<Tab
|
||||
currentTab={currentTab}
|
||||
key={index}
|
||||
label={tab.props.label}
|
||||
onClick={(nextTab) => onClick(nextTab, tab.props.name)}
|
||||
navbarTab={props.navbar || false}
|
||||
/>
|
||||
))}
|
||||
{reminderInfo}
|
||||
</div>
|
||||
<div className="modalTabContent">
|
||||
<div className="modalNavbar">
|
||||
{navbarButtons.map(({ tab, icon }, index) => (
|
||||
<Button
|
||||
type="navigation"
|
||||
onClick={() => props.changeTab(tab)}
|
||||
icon={icon}
|
||||
label={variables.getMessage(`modals.main.navbar.${tab}`)}
|
||||
active={props.current === tab}
|
||||
key={`${tab}-${index}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{props.children.map((tab, index) => {
|
||||
if (tab.props.label !== currentTab) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary key={`error-boundary-${index}`}>{tab.props.children}</ErrorBoundary>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
107
src/components/Elements/MainModal/backend/newTabs.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState, useCallback, memo, useMemo } from 'react';
|
||||
import variables from 'config/variables';
|
||||
import Tab from './Tab';
|
||||
import { useTab } from './TabContext';
|
||||
import { MdOutlineWarning, MdRefresh, MdClose } from 'react-icons/md';
|
||||
import Stats from 'features/stats/api/stats';
|
||||
|
||||
const Sidebar = memo(({ sections, currentTab, setCurrentTab }) => {
|
||||
const { subTab, setSubTab, setSubSection } = useTab();
|
||||
const handleClick = useCallback(
|
||||
(label) => () => {
|
||||
const newTab = variables.getMessage(label);
|
||||
setSubTab(newTab);
|
||||
setSubSection('');
|
||||
Stats.postEvent('settings-tab', newTab, 'opened');
|
||||
},
|
||||
[setSubTab, setSubSection],
|
||||
);
|
||||
|
||||
const hideReminder = () => {
|
||||
localStorage.setItem('showReminder', false);
|
||||
document.querySelector('.reminder-info').style.display = 'none';
|
||||
};
|
||||
|
||||
const reminderInfo = useMemo(
|
||||
() => (
|
||||
<div
|
||||
className="bg-rose-800 border-rose-700 border-2 flex-row px-10 py-5 rounded items-center justify-between"
|
||||
style={{ display: localStorage.getItem('showReminder') === 'true' ? 'flex' : 'none' }}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<MdOutlineWarning />
|
||||
<span>{variables.getMessage('settings:reminder.message')}</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<button
|
||||
className="bg-neutral-900 border-neutral-800 border-2 px-8 py-2 flex flex-row items-center gap-2 rounded"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<MdRefresh /> {variables.getMessage('modals.main.error_boundary.refresh')}
|
||||
</button>
|
||||
<button
|
||||
className="bg-neutral-900 border-neutral-800 border-2 px-8 py-2 flex flex-row items-center gap-2 rounded"
|
||||
onClick={hideReminder}
|
||||
>
|
||||
<MdClose />
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="modalSidebar">
|
||||
{sections.map((section, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
currentTab={subTab}
|
||||
label={variables.getMessage(section.label)}
|
||||
onClick={handleClick(section.label)}
|
||||
navbarTab={section.navbar || false}
|
||||
/>
|
||||
))}
|
||||
{reminderInfo}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Content = memo(({ sections, currentTab }) => {
|
||||
const hideReminder = () => {
|
||||
localStorage.setItem('showReminder', false);
|
||||
document.querySelector('.reminder-info').style.display = 'none';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{sections.map(
|
||||
({ label, name, component: Component }) =>
|
||||
variables.getMessage(label) === currentTab && (
|
||||
<div
|
||||
className="w-full rounded h-[calc(78vh-80px)] flex flex-col pr-10 gap-3 lg:overflow-x-hidden overflow-y-auto overflow-x-auto"
|
||||
key={name}
|
||||
label={variables.getMessage(label)}
|
||||
name={name}
|
||||
>
|
||||
<Component />
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const Tabs = ({ sections }) => {
|
||||
const { subTab, setSubTab, setSubSection } = useTab();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<Sidebar sections={sections} currentTab={subTab} />
|
||||
<Content sections={sections} currentTab={subTab} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Tabs };
|
||||
@@ -27,6 +27,7 @@
|
||||
z-index: -2;
|
||||
transition-timing-function: ease-in;
|
||||
border-radius: map-get($modal, 'border-radius');
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
transform: scale(0);
|
||||
@@ -37,6 +38,7 @@
|
||||
}
|
||||
|
||||
.modalInfoPage {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
@@ -47,13 +49,46 @@
|
||||
right: 3rem;
|
||||
}
|
||||
|
||||
.ReactModal__Html--open,
|
||||
.ReactModal__Body--open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* modal transition */
|
||||
.ReactModal__Content--after-open {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.ReactModal__Content--before-close {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
#modal {
|
||||
height: 80vh;
|
||||
width: clamp(60vw, 1400px, 90vw);
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-background);
|
||||
}
|
||||
}
|
||||
|
||||
.closePositioning {
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 3rem;
|
||||
}
|
||||
|
||||
.closeModal {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0.5em;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: 0.5s;
|
||||
align-self: flex-end;
|
||||
|
||||
svg {
|
||||
font-size: 2em;
|
||||
@@ -82,15 +117,6 @@
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
#modal {
|
||||
height: 80vh;
|
||||
width: clamp(60vw, 1400px, 90vw);
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-background);
|
||||
}
|
||||
}
|
||||
|
||||
/* fixes for font size on extension */
|
||||
label,
|
||||
p,
|
||||
@@ -124,9 +150,9 @@ h5 {
|
||||
height: 50px;
|
||||
|
||||
@include themed {
|
||||
border: 3px solid t($modal-sidebar);
|
||||
border: 3px solid t($btn-background);
|
||||
border-radius: 50%;
|
||||
border-top-color: t($modal-sidebarActive);
|
||||
border-top-color: t($brand-accent);
|
||||
}
|
||||
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
@@ -247,13 +273,12 @@ h5 {
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
padding: 25px;
|
||||
margin-top: 20px;
|
||||
transition: 0.5s;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
//box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
@@ -269,6 +294,7 @@ h5 {
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
flex-grow: 0;
|
||||
|
||||
svg {
|
||||
@include themed {
|
||||
@@ -291,14 +317,14 @@ h5 {
|
||||
flex-flow: row;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
width: 275px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-info {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 15px;
|
||||
gap: 15px;
|
||||
|
||||
|
||||
@@ -51,31 +51,12 @@
|
||||
@include themed {
|
||||
background-color: t($modal-secondaryColour);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
|
||||
&:hover {
|
||||
background-color: t($modal-sidebarActive);
|
||||
|
||||
img {
|
||||
background-color: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.item-back {
|
||||
filter: blur(60px) saturate(180%) brightness(90%);
|
||||
position: absolute;
|
||||
object-fit: cover !important;
|
||||
height: 90px;
|
||||
width: 100px;
|
||||
border-radius: 100px;
|
||||
transition: 0.5s;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
object-fit: cover !important;
|
||||
height: 60px !important;
|
||||
@@ -125,15 +106,15 @@
|
||||
}
|
||||
|
||||
.itemPage {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
// display: flex;
|
||||
// flex-flow: row;
|
||||
// justify-content: space-between;
|
||||
|
||||
.itemShowcase {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 25px;
|
||||
width: 60%;
|
||||
// width: 60%;
|
||||
max-width: 650px;
|
||||
|
||||
.description {
|
||||
@@ -162,31 +143,24 @@
|
||||
border-radius: 15px;
|
||||
width: 30%;
|
||||
max-width: 300px;
|
||||
max-height: 700px;
|
||||
// max-height: 700px;
|
||||
|
||||
.front {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
box-sizing: border-box !important;
|
||||
border-radius: 12px 12px 0 0;
|
||||
backdrop-filter: blur(40px) saturate(150%) brightness(75%);
|
||||
|
||||
@include themed {
|
||||
background-image: linear-gradient(to bottom, transparent, t($modal-background));
|
||||
}
|
||||
}
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
box-sizing: border-box !important;
|
||||
border-radius: 12px;
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 5px 25px black;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.75);
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.divider {
|
||||
@@ -435,35 +409,6 @@ p.author {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.marketplaceSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 30px;
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
|
||||
@include themed {
|
||||
box-shadow: 0 0 0 3px t($modal-sidebarActive);
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
|
||||
input {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
&:focus-within {
|
||||
background: t($modal-sidebarActive);
|
||||
box-shadow: 0 0 0 1px t($color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: t($modal-sidebarActive);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inCollection {
|
||||
// background-image: linear-gradient(to left, transparent, #000),
|
||||
// url('https://external-preview.redd.it/JyhsEoGMhKIMi3kvfBS24L0IllAO_KrIm4UI-dA1Ax4.jpg?auto=webp&s=b5adf9859b2c1855a5b3085f9453a6e878548505');
|
||||
@@ -507,7 +452,6 @@ p.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.tooltip {
|
||||
margin-right: 25px;
|
||||
|
||||
@@ -9,9 +9,11 @@ p.description {
|
||||
}
|
||||
|
||||
.moreInfo {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 30px;
|
||||
.details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.items {
|
||||
margin-top: 0 !important;
|
||||
|
||||
@@ -1,67 +1,43 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.modalTabContent {
|
||||
width: 100% !important;
|
||||
.settingsRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
justify-content: space-between;
|
||||
transition: 0.4s ease-in-out;
|
||||
@extend %tabText;
|
||||
|
||||
/* button {
|
||||
@include modal-button(standard);
|
||||
} */
|
||||
/* border-top: 1px solid #ccc; */
|
||||
border-bottom: 1px solid #676767;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
@include themed {
|
||||
padding: 1rem 3rem 3rem;
|
||||
&.settingsNoBorder {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: t($modal-background);
|
||||
flex-flow: column;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
@extend %tabText;
|
||||
.action {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: flex-end;
|
||||
width: 300px;
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
background: rgb(196 196 196 / 74%);
|
||||
outline: none;
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.settingsRow {
|
||||
.link {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
justify-content: space-between;
|
||||
transition: 0.4s ease-in-out;
|
||||
|
||||
/* border-top: 1px solid #ccc; */
|
||||
border-bottom: 1px solid #676767;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
&.settingsNoBorder {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: flex-end;
|
||||
width: 300px;
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,60 +57,61 @@
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
.itemPage {
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
|
||||
@include themed {
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
tr:first-child {
|
||||
@include themed {
|
||||
border-radius: t($borderRadius);
|
||||
color: t($subColor);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
letter-spacing: 2px;
|
||||
|
||||
th {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
tr {
|
||||
th:last-child {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
tr:not(:first-child) {
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 90%;
|
||||
margin: 10px;
|
||||
|
||||
tr:first-child {
|
||||
@include themed {
|
||||
color: t($color);
|
||||
border-radius: t($borderRadius);
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
letter-spacing: 2px;
|
||||
|
||||
th {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
tr {
|
||||
th:last-child {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
tr:not(:first-child) {
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 90%;
|
||||
margin: 10px;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,3 @@
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.modalNavbar {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 25px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
position: sticky;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
background: t($modal-sidebar);
|
||||
border-radius: 12px 0 0 12px;
|
||||
overflow: hidden auto;
|
||||
height: 80vh;
|
||||
height: calc(80vh - 80px);
|
||||
min-width: 250px;
|
||||
|
||||
.mainTitle {
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
.updateChangelog {
|
||||
max-width: 75%;
|
||||
margin-top: 15px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 18px;
|
||||
|
||||
a {
|
||||
color: var(--modal-link);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.changelogtab {
|
||||
.mainTitle {
|
||||
margin: 0 !important;
|
||||
@@ -22,6 +7,12 @@
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.changelogAt {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,17 @@
|
||||
-webkit-padding-start: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.images-row.fixed-width {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.images-row {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Tooltip, Button } from 'components/Elements';
|
||||
|
||||
function ResetModal({ modalClose }) {
|
||||
const reset = () => {
|
||||
variables.stats.postEvent('setting', 'Reset');
|
||||
variables.stats.postEvent('setting', 'reset');
|
||||
setDefaultSettings('reset');
|
||||
window.location.reload();
|
||||
};
|
||||
@@ -15,34 +15,32 @@ function ResetModal({ modalClose }) {
|
||||
<div className="smallModal">
|
||||
<div className="shareHeader">
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.title')}
|
||||
{variables.getMessage('settings:sections.advanced.reset_modal.title')}
|
||||
</span>
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
|
||||
>
|
||||
<Tooltip title={variables.getMessage('settings:sections.advanced.reset_modal.cancel')}>
|
||||
<div className="close" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.question')}
|
||||
{variables.getMessage('settings:sections.advanced.reset_modal.question')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.information')}
|
||||
{variables.getMessage('settings:sections.advanced.reset_modal.information')}
|
||||
</span>
|
||||
<div className="resetFooter">
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={modalClose}
|
||||
icon={<MdClose />}
|
||||
label={variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
|
||||
label={variables.getMessage('settings:sections.advanced.reset_modal.cancel')}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => reset()}
|
||||
icon={<MdRestartAlt />}
|
||||
label={variables.getMessage('modals.main.settings.buttons.reset')}
|
||||
label={variables.getMessage('settings:buttons.reset')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ function ShareModal({ modalClose, data }) {
|
||||
<div className="smallModal">
|
||||
<div className="shareHeader">
|
||||
<span className="title">{variables.getMessage('widgets.quote.share')}</span>
|
||||
<Tooltip title={variables.getMessage('modals.welcome.buttons.close')}>
|
||||
<Tooltip title={variables.getMessage('welcome:buttons.close')}>
|
||||
<div className="close" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, memo, useRef } from 'react';
|
||||
import { useFloating, flip, offset, shift } from '@floating-ui/react-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import './tooltip.scss';
|
||||
|
||||
function Tooltip({ children, title, style, placement, subtitle }) {
|
||||
@@ -17,7 +18,7 @@ function Tooltip({ children, title, style, placement, subtitle }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
<motion.div
|
||||
className="tooltip"
|
||||
style={style}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
@@ -26,25 +27,34 @@ function Tooltip({ children, title, style, placement, subtitle }) {
|
||||
onBlur={() => setShowTooltip(false)}
|
||||
ref={setReference}
|
||||
aria-describedby={tooltipId.current}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{showTooltip && (
|
||||
<span
|
||||
ref={refs.setFloating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? '',
|
||||
left: x ?? '',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
}}
|
||||
className="tooltipTitle"
|
||||
>
|
||||
{title}
|
||||
<span style={{ fontSize: '8px' }}>{subtitle}</span>
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
<AnimatePresence>
|
||||
{showTooltip && (
|
||||
<motion.span
|
||||
ref={refs.setFloating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? '',
|
||||
left: x ?? '',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
}}
|
||||
className="tooltipTitle"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{title}
|
||||
<span style={{ fontSize: '8px' }}>{subtitle}</span>
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
.tooltipTitle {
|
||||
@extend %basic;
|
||||
border-radius: 8px !important;
|
||||
|
||||
text-align: center;
|
||||
font-size: 0.6rem;
|
||||
@@ -33,9 +34,6 @@
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
opacity: 1;
|
||||
animation-name: floating;
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
#modal {
|
||||
@@ -54,26 +52,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipTitle::before {
|
||||
transform: scale3d(0.2, 0.2, 1);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.tooltipTitle::after {
|
||||
transform: translate3d(0, 6px, 0);
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.tooltipTitle:hover::before,
|
||||
.tooltipTitle:hover::after {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
|
||||
.tooltipTitle:hover::after {
|
||||
transition: all 0.2s 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
#arrow {
|
||||
position: absolute;
|
||||
background: #333;
|
||||
@@ -96,37 +74,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#floating {
|
||||
transition-property: opacity, transform;
|
||||
}
|
||||
|
||||
#floating[data-status='open'],
|
||||
#floating[data-status='close'] {
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
#floating[data-status='initial'],
|
||||
#floating[data-status='close'] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#floating[data-status='initial'][data-placement^='top'],
|
||||
#floating[data-status='close'][data-placement^='top'] {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
#floating[data-status='initial'][data-placement^='bottom'],
|
||||
#floating[data-status='close'][data-placement^='bottom'] {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
#floating[data-status='initial'][data-placement^='left'],
|
||||
#floating[data-status='close'][data-placement^='left'] {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
#floating[data-status='initial'][data-placement^='right'],
|
||||
#floating[data-status='close'][data-placement^='right'] {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
68
src/components/Form/Settings/Checkbox/Checkbox.cy.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
/* eslint-disable no-undef */
|
||||
import React from 'react'
|
||||
import { Checkbox } from './Checkbox'
|
||||
|
||||
describe('<Checkbox />', () => {
|
||||
it('renders', () => {
|
||||
// see: https://on.cypress.io/mounting-react
|
||||
cy.mount(<Checkbox />)
|
||||
})
|
||||
|
||||
// checked prop works
|
||||
it('can init checked', () => {
|
||||
cy.mount(<Checkbox checked={true} />)
|
||||
cy.get('span').should('have.attr', 'aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('can init unchecked', () => {
|
||||
cy.mount(<Checkbox checked={false} />)
|
||||
cy.get('span').should('have.attr', 'aria-checked', 'false')
|
||||
})
|
||||
|
||||
// can init with setting
|
||||
it('can init with setting', () => {
|
||||
localStorage.setItem('test', 'true')
|
||||
cy.mount(<Checkbox name="test" />)
|
||||
cy.get('span').should('have.attr', 'aria-checked', 'true')
|
||||
})
|
||||
|
||||
// can be changed
|
||||
it('can be changed', () => {
|
||||
cy.mount(<Checkbox checked={true} />)
|
||||
cy.get('span').click().should('have.attr', 'aria-checked', 'false')
|
||||
cy.get('span').click().should('have.attr', 'aria-checked', 'true')
|
||||
})
|
||||
|
||||
// onChange callback works
|
||||
it('calls onChange when clicked', () => {
|
||||
const onChange = cy.stub()
|
||||
cy.mount(<Checkbox onChange={onChange} />)
|
||||
cy.get('span').click().then(() => {
|
||||
expect(onChange).to.be.called()
|
||||
})
|
||||
})
|
||||
|
||||
// disabled prop works
|
||||
|
||||
|
||||
// text prop works
|
||||
it('displays text', () => {
|
||||
cy.mount(<Checkbox text="Hello, world!" />)
|
||||
cy.contains('Hello, world!').should('be.visible')
|
||||
})
|
||||
|
||||
// reminder works (needs stub)
|
||||
|
||||
// reminder works (needs stub)
|
||||
|
||||
// category prop works (needs stub)
|
||||
|
||||
// name prop works
|
||||
it('saves to localStorage with name', () => {
|
||||
cy.mount(<Checkbox name="test" />)
|
||||
cy.get('span').click()
|
||||
expect(localStorage.getItem('test')).to.eq('true')
|
||||
})
|
||||
|
||||
// stats tracking works (needs stub)
|
||||
})
|
||||
@@ -1,61 +1,51 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { Checkbox as CheckboxUI, FormControlLabel } from '@mui/material';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Checkbox as CheckboxUI, Field, Label } from '@headlessui/react';
|
||||
import { MdCheckBox } from 'react-icons/md';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class Checkbox extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
checked: localStorage.getItem(this.props.name) === 'true',
|
||||
};
|
||||
}
|
||||
const Checkbox = (props) => {
|
||||
const [checked, setChecked] = useState(localStorage.getItem(props.name) === 'true');
|
||||
|
||||
handleChange = () => {
|
||||
const value = this.state.checked !== true;
|
||||
localStorage.setItem(this.props.name, value);
|
||||
useEffect(() => {
|
||||
setChecked(localStorage.getItem(props.name) === 'true');
|
||||
}, [props.name]);
|
||||
|
||||
this.setState({
|
||||
checked: value,
|
||||
});
|
||||
const handleChange = () => {
|
||||
const value = !checked;
|
||||
localStorage.setItem(props.name, value.toString());
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
setChecked(value);
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(value);
|
||||
}
|
||||
|
||||
variables.stats.postEvent(
|
||||
'setting',
|
||||
`${this.props.name} ${this.state.checked === true ? 'enabled' : 'disabled'}`,
|
||||
);
|
||||
variables.stats.postEvent('setting', props.name, value ? 'enabled' : 'disabled');
|
||||
|
||||
if (this.props.element) {
|
||||
if (!document.querySelector(this.props.element)) {
|
||||
if (props.element) {
|
||||
if (!document.querySelector(props.element)) {
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
return localStorage.setItem('showReminder', true);
|
||||
localStorage.setItem('showReminder', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.emit('refresh', this.props.category);
|
||||
EventBus.emit('refresh', props.category);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<CheckboxUI
|
||||
name={this.props.name}
|
||||
color="primary"
|
||||
className="checkbox"
|
||||
checked={this.state.checked}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.disabled || false}
|
||||
/>
|
||||
}
|
||||
label={this.props.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Field className="w-[300px] h-9 my-1 flex flex-row-reverse items-center gap-2 justify-between text-left">
|
||||
<CheckboxUI
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={props.disabled || false}
|
||||
className="border border-[#484848] bg-white/5 group size-4 rounded-sm data-[checked]:bg-neutral-900 cursor-pointer grid place-content-center"
|
||||
>
|
||||
<MdCheckBox className="stroke-white opacity-0 group-data-[checked]:opacity-100 size-6" />
|
||||
</CheckboxUI>
|
||||
<Label>{props.text}</Label>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export { Checkbox as default, Checkbox };
|
||||
|
||||
@@ -1,78 +1,86 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { InputLabel, MenuItem, FormControl, Select } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { Field, Label, Select } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
import variables from 'config/variables';
|
||||
|
||||
class Dropdown extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: localStorage.getItem(this.props.name) || this.props.items[0].value,
|
||||
title: '',
|
||||
};
|
||||
this.dropdown = createRef();
|
||||
}
|
||||
const Dropdown = (props) => {
|
||||
const [value, setValue] = useState(localStorage.getItem(props.name) || props.items[0]?.value);
|
||||
|
||||
onChange = (e) => {
|
||||
const { value } = e.target;
|
||||
|
||||
if (value === variables.getMessage('modals.main.loading')) {
|
||||
const handleChange = (e) => {
|
||||
const newValue = e.target.value;
|
||||
if (newValue === variables.getMessage('modals.main.loading')) {
|
||||
return;
|
||||
}
|
||||
|
||||
variables.stats.postEvent('setting', `${this.props.name} from ${this.state.value} to ${value}`);
|
||||
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
|
||||
setValue(newValue);
|
||||
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
|
||||
if (!this.props.noSetting) {
|
||||
localStorage.setItem(this.props.name, value);
|
||||
localStorage.setItem(this.props.name2, this.props.value2);
|
||||
if (!props.noSetting) {
|
||||
localStorage.setItem(props.name, newValue);
|
||||
localStorage.setItem(props.name2, props.value2);
|
||||
}
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
if (props.onChange) {
|
||||
props.onChange(newValue);
|
||||
}
|
||||
|
||||
if (this.props.element) {
|
||||
if (!document.querySelector(this.props.element)) {
|
||||
if (props.element) {
|
||||
if (!document.querySelector(props.element)) {
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
return localStorage.setItem('showReminder', true);
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.emit('refresh', this.props.category);
|
||||
EventBus.emit('refresh', props.category);
|
||||
};
|
||||
|
||||
render() {
|
||||
const id = 'dropdown' + this.props.name;
|
||||
const label = this.props.label || '';
|
||||
const selectedItem = props.items.find((item) => item?.value === value);
|
||||
|
||||
return (
|
||||
<FormControl fullWidth className={id}>
|
||||
<InputLabel id={id}>{label}</InputLabel>
|
||||
return (
|
||||
<Field className="w-[100%] max-w-md mr-10">
|
||||
{props.label && <Label className="mb-2 block text-sm font-medium">{props.label}</Label>}
|
||||
<div className="relative">
|
||||
<Select
|
||||
labelId={id}
|
||||
id={this.props.name}
|
||||
value={this.state.value}
|
||||
label={label}
|
||||
onChange={this.onChange}
|
||||
ref={this.dropdown}
|
||||
key={id}
|
||||
name={props.name}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
aria-label={props.label || props.name}
|
||||
className={clsx(
|
||||
'w-full rounded-lg py-4 px-5 text-sm/6 font-semibold text-white shadow-md',
|
||||
'bg-white/5 hover:bg-white/10 dark:text-white',
|
||||
'border border-white/10',
|
||||
'transition-colors duration-200',
|
||||
'focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white',
|
||||
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
|
||||
'appearance-none',
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='2' stroke='white' class='w-6 h-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19.5 8.25l-7.5 7.5-7.5-7.5' /%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'right 1.25rem center',
|
||||
backgroundSize: '1rem',
|
||||
}}
|
||||
>
|
||||
{this.props.items.map((item) =>
|
||||
{props.items.map((item) =>
|
||||
item !== null ? (
|
||||
<MenuItem key={id + item.value} value={item.value}>
|
||||
<option
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
className="bg-modal-content-light dark:bg-modal-content-dark"
|
||||
>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</option>
|
||||
) : null,
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
{selectedItem?.description && (
|
||||
<p className="mt-2 text-sm text-white/50">{selectedItem.description}</p>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export { Dropdown as default, Dropdown };
|
||||
|
||||
@@ -1,65 +1,53 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { compressAccurately, filetoDataURL } from 'image-conversion';
|
||||
import videoCheck from 'features/background/api/videoCheck';
|
||||
|
||||
class FileUpload extends PureComponent {
|
||||
componentDidMount() {
|
||||
document.getElementById(this.props.id).onchange = (e) => {
|
||||
const reader = new FileReader();
|
||||
const file = e.target.files[0];
|
||||
function FileUpload({ id, type, accept, loadFunction }) {
|
||||
const handleChange = (e) => {
|
||||
const reader = new FileReader();
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (this.props.type === 'settings') {
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
reader.onload = (e) => {
|
||||
return this.props.loadFunction(e.target.result);
|
||||
};
|
||||
} else {
|
||||
// background upload
|
||||
const settings = {};
|
||||
if (type === 'settings') {
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
reader.onload = (e) => {
|
||||
return loadFunction(e.target.result);
|
||||
};
|
||||
} else {
|
||||
// background upload
|
||||
const settings = {};
|
||||
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
settings[key] = localStorage.getItem(key);
|
||||
});
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
settings[key] = localStorage.getItem(key);
|
||||
});
|
||||
|
||||
const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length;
|
||||
if (videoCheck(file.type) === true) {
|
||||
if (settingsSize + file.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
return this.props.loadFunction(file);
|
||||
const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length;
|
||||
if (videoCheck(file.type) === true) {
|
||||
if (settingsSize + file.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
compressAccurately(file, {
|
||||
size: 450,
|
||||
accuracy: 0.9,
|
||||
}).then(async (res) => {
|
||||
if (settingsSize + res.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
this.props.loadFunction({
|
||||
target: {
|
||||
result: await filetoDataURL(res),
|
||||
},
|
||||
});
|
||||
});
|
||||
return loadFunction(file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<input
|
||||
id={this.props.id}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
accept={this.props.accept}
|
||||
/>
|
||||
);
|
||||
}
|
||||
loadFunction(
|
||||
{
|
||||
target: {
|
||||
result: file,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
accept={accept}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { FileUpload as default, FileUpload };
|
||||
|
||||
@@ -1,87 +1,82 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import {
|
||||
Radio as RadioUI,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
} from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { Radio as PureRadio, RadioGroup } from '@headlessui/react';
|
||||
import { MdCheckCircle } from 'react-icons/md';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { translations } from 'lib/translations';
|
||||
|
||||
class Radio extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: localStorage.getItem(this.props.name),
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = async (e) => {
|
||||
const { value } = e.target;
|
||||
function Radio(props) {
|
||||
const [value, setValue] = useState(localStorage.getItem(props.name));
|
||||
|
||||
const handleChange = async (value) => {
|
||||
if (value === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.name === 'language') {
|
||||
if (props.name === 'language') {
|
||||
// old tab name
|
||||
if (localStorage.getItem('tabName') === variables.getMessage('tabname')) {
|
||||
localStorage.setItem('tabName', translations[value.replace('-', '_')].tabname);
|
||||
}
|
||||
// TODO: was this important?
|
||||
// if (localStorage.getItem('tabName') === variables.getMessage('tabname')) {
|
||||
// localStorage.setItem('tabName', translations[value].tabname);
|
||||
// }
|
||||
}
|
||||
|
||||
localStorage.setItem(this.props.name, value);
|
||||
localStorage.setItem(props.name, value);
|
||||
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
setValue(value);
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
if (props.onChange) {
|
||||
props.onChange(value);
|
||||
}
|
||||
|
||||
variables.stats.postEvent('setting', `${this.props.name} from ${this.state.value} to ${value}`);
|
||||
if (props.name !== 'language') {
|
||||
variables.stats.postEvent('setting', props.name, `${value}-${value}`);
|
||||
} else {
|
||||
variables.stats.postEvent('language', props.name, `${value}-${value}`);
|
||||
}
|
||||
|
||||
if (this.props.element) {
|
||||
if (!document.querySelector(this.props.element)) {
|
||||
if (props.element) {
|
||||
if (!document.querySelector(props.element)) {
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
return localStorage.setItem('showReminder', true);
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.emit('refresh', this.props.category);
|
||||
EventBus.emit('refresh', props.category);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel
|
||||
className={this.props.smallTitle ? 'radio-title-small' : 'radio-title'}
|
||||
component="legend"
|
||||
>
|
||||
{this.props.title}
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label={this.props.name}
|
||||
name={this.props.name}
|
||||
onChange={this.handleChange}
|
||||
value={this.state.value}
|
||||
>
|
||||
{this.props.options.map((option) => (
|
||||
<FormControlLabel
|
||||
value={option.value}
|
||||
control={<RadioUI />}
|
||||
label={option.name}
|
||||
key={option.name}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-full">
|
||||
<RadioGroup
|
||||
aria-label={props.name}
|
||||
name={props.name}
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
className="space-y-2"
|
||||
>
|
||||
{props.options.map((option) => (
|
||||
<PureRadio
|
||||
key={option.name}
|
||||
label={option.name}
|
||||
value={option.value}
|
||||
className="data-[checked]:bg-white/10 group relative flex cursor-pointer rounded-lg bg-white/5 dark:data-[checked]:hover:bg-neutral-700 dark:hover:bg-neutral-700 dark:data-[checked]:hover:bg-neutral-700 hover:bg-neutral-200 py-4 px-5 dark:text-white text-black shadow-md transition focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="text-sm/6">
|
||||
<p className="font-semibold capitalize">{option.name}</p>
|
||||
<div className="flex gap-2 dark:text-white/50">
|
||||
<div>{option.subname}</div>
|
||||
<div aria-hidden="true">·</div>
|
||||
<div>10%</div>
|
||||
</div>
|
||||
</div>
|
||||
<MdCheckCircle className="size-6 dark:fill-white fill-black opacity-0 transition group-data-[checked]:opacity-100" />
|
||||
</div>
|
||||
</PureRadio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Radio as default, Radio };
|
||||
|
||||
@@ -1,88 +1,361 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { useState, useCallback, memo, useMemo, useRef } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Slider } from '@mui/material';
|
||||
import { MdRefresh } from 'react-icons/md';
|
||||
|
||||
import ReactSlider from 'react-slider';
|
||||
import { MdRefresh, MdEdit } from 'react-icons/md';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
class SliderComponent extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: localStorage.getItem(this.props.name) || this.props.default,
|
||||
};
|
||||
}
|
||||
// Style definitions split into logical groups for better readability
|
||||
const buttonStyles = {
|
||||
base: 'inline-flex items-center text-sm px-3 py-1.5 rounded-md font-medium',
|
||||
colors: 'bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400',
|
||||
darkMode: 'dark:bg-white/10 dark:hover:bg-white/15 dark:active:bg-white/20',
|
||||
text: 'text-neutral-800 dark:text-white',
|
||||
transitions: 'transition-colors duration-200',
|
||||
};
|
||||
|
||||
handleChange = (e, text) => {
|
||||
let { value } = e.target;
|
||||
value = Number(value);
|
||||
const thumbStyles = {
|
||||
base: [
|
||||
'w-5 h-5 rounded-full cursor-pointer',
|
||||
'absolute top-1/2',
|
||||
'flex items-center justify-center',
|
||||
'-translate-y-1/2', // Keep only Y transform in classes
|
||||
],
|
||||
colors: 'bg-gradient-to-br from-neutral-50 to-neutral-200 dark:from-white dark:to-neutral-100',
|
||||
interactions: [
|
||||
'hover:from-neutral-100 hover:to-neutral-300',
|
||||
'dark:hover:from-white dark:hover:to-neutral-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-neutral-400 dark:focus:ring-white/40',
|
||||
],
|
||||
// Remove transition-all which was causing lag
|
||||
effects: 'shadow-lg shadow-black/10 dark:shadow-black/25',
|
||||
tooltip: [
|
||||
'before:content-[attr(aria-valuenow)]',
|
||||
'before:absolute before:top-[-28px]',
|
||||
'before:text-xs before:bg-neutral-800 dark:before:bg-black/90',
|
||||
'before:text-white before:px-2 before:py-1 before:rounded-md',
|
||||
'before:opacity-0 hover:before:opacity-100',
|
||||
'before:transition-all before:duration-200 before:whitespace-nowrap',
|
||||
'before:shadow-lg',
|
||||
],
|
||||
};
|
||||
|
||||
if (text) {
|
||||
if (value === '') {
|
||||
return this.setState({
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
const markStyles = {
|
||||
base: [
|
||||
'h-3 w-1.5 rounded-full cursor-pointer select-none',
|
||||
'absolute top-1/2 transform -translate-y-1/2',
|
||||
],
|
||||
colors: 'bg-neutral-400 dark:bg-white/30',
|
||||
hover: 'hover:bg-neutral-600 dark:hover:bg-white/50 hover:scale-110',
|
||||
transitions: 'hover:transition-transform hover:duration-200',
|
||||
label: [
|
||||
'after:content-[attr(data-value)]',
|
||||
'after:absolute after:top-5',
|
||||
'after:text-xs after:opacity-85',
|
||||
'after:left-1/2', // Add this to position relative to center
|
||||
'after:transform after:-translate-x-1/2', // Center the label
|
||||
'after:whitespace-nowrap after:pointer-events-none',
|
||||
'after:font-medium',
|
||||
'after:text-center', // Ensure text is centered
|
||||
'after:text-neutral-700 dark:after:text-white/85',
|
||||
],
|
||||
};
|
||||
|
||||
if (value > this.props.max) {
|
||||
value = this.props.max;
|
||||
}
|
||||
const MULTIPLIER_MARKS = {
|
||||
10: '0.1x',
|
||||
100: '1x',
|
||||
200: '2x',
|
||||
400: '4x',
|
||||
};
|
||||
|
||||
if (value < this.props.min) {
|
||||
value = this.props.min;
|
||||
}
|
||||
}
|
||||
// Update ValueDisplay component to show edit affordance
|
||||
const ValueDisplay = memo(({ value, display, onChange, min, max }) => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
localStorage.setItem(this.props.name, value);
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
|
||||
if (this.props.element) {
|
||||
if (!document.querySelector(this.props.element)) {
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
return localStorage.setItem('showReminder', true);
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.emit('refresh', this.props.category);
|
||||
const handleChange = (e) => {
|
||||
// Allow typing any numeric input, validate on blur
|
||||
const numericValue = e.target.value.replace(/[^\d.-]/g, '');
|
||||
setInputValue(numericValue);
|
||||
};
|
||||
|
||||
resetItem = () => {
|
||||
this.handleChange({
|
||||
target: {
|
||||
value: this.props.default || '',
|
||||
},
|
||||
});
|
||||
const handleBlur = () => {
|
||||
setEditing(false);
|
||||
const numValue = Number(inputValue);
|
||||
|
||||
// Only validate and clamp on blur
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.min(Math.max(numValue, min), max);
|
||||
setInputValue(clampedValue);
|
||||
onChange(clampedValue);
|
||||
} else {
|
||||
setInputValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleBlur();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditing(false);
|
||||
setInputValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
return editing ? (
|
||||
<input
|
||||
type="number" // Change to number type
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
min={min} // Optional: add min/max if needed
|
||||
max={max}
|
||||
step="any" // Allow decimal values
|
||||
className="w-20 text-sm bg-neutral-200 dark:bg-white/15 text-neutral-800 dark:text-white
|
||||
px-3 py-1.5 rounded-md font-medium outline-none focus:ring-2
|
||||
focus:ring-neutral-400 dark:focus:ring-white/40
|
||||
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none
|
||||
transition-colors duration-200" // Added transition
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setEditing(true)}
|
||||
className="group relative flex items-center gap-2 text-sm bg-neutral-200 dark:bg-white/15
|
||||
text-neutral-800 dark:text-white px-3 py-1.5 rounded-md font-medium cursor-pointer
|
||||
hover:bg-neutral-300 dark:hover:bg-white/20 transition-colors duration-200"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
{value}
|
||||
<span className="text-neutral-500 dark:text-white/50 ml-0.5">{display}</span>
|
||||
</span>
|
||||
<MdEdit className="w-4 h-4 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
<span
|
||||
className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-neutral-800
|
||||
dark:bg-black/90 text-white text-xs rounded opacity-0 group-hover:opacity-100
|
||||
transition-opacity duration-200 whitespace-nowrap"
|
||||
>
|
||||
Click to edit
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Memoized reset button component
|
||||
const ResetButton = memo(({ onClick }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
buttonStyles.base,
|
||||
buttonStyles.colors,
|
||||
buttonStyles.darkMode,
|
||||
buttonStyles.text,
|
||||
buttonStyles.transitions,
|
||||
)}
|
||||
>
|
||||
<MdRefresh className="w-4 h-4 mr-1.5" />
|
||||
{variables.getMessage('settings:buttons.reset')}
|
||||
</button>
|
||||
));
|
||||
|
||||
function SliderComponent(props) {
|
||||
const [value, setValue] = useState(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(props.name);
|
||||
return stored ? Number(stored) : Number(props.default);
|
||||
} catch (e) {
|
||||
return Number(props.default);
|
||||
}
|
||||
});
|
||||
|
||||
const isDraggingRef = useRef(false);
|
||||
const lastUpdateRef = useRef(value);
|
||||
|
||||
// Separated storage update from visual update
|
||||
const updateStorage = useCallback(
|
||||
(newValue) => {
|
||||
try {
|
||||
localStorage.setItem(props.name, newValue);
|
||||
lastUpdateRef.current = newValue;
|
||||
|
||||
if (props.element && !document.querySelector(props.element)) {
|
||||
const reminderElement = document.querySelector('.reminder-info');
|
||||
if (reminderElement) {
|
||||
reminderElement.style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.emit('refresh', props.category);
|
||||
} catch (e) {
|
||||
console.error('Error updating slider:', e);
|
||||
}
|
||||
},
|
||||
[props.element, props.category],
|
||||
);
|
||||
|
||||
// More efficient debounced storage update
|
||||
const debouncedUpdate = useMemo(() => debounce(updateStorage, 150), [updateStorage]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
if (typeof newValue !== 'number') return;
|
||||
const clampedValue = Math.min(Math.max(newValue, props.min), props.max);
|
||||
|
||||
// Always update visual state immediately
|
||||
setValue(clampedValue);
|
||||
|
||||
// Only update storage if we're not actively dragging
|
||||
if (!isDraggingRef.current) {
|
||||
debouncedUpdate(clampedValue);
|
||||
}
|
||||
},
|
||||
[props.min, props.max, debouncedUpdate],
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
isDraggingRef.current = true;
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
isDraggingRef.current = false;
|
||||
// Ensure final value is stored
|
||||
if (lastUpdateRef.current !== value) {
|
||||
updateStorage(value);
|
||||
}
|
||||
}, [value, updateStorage]);
|
||||
|
||||
// Update thumbStyles to include performance optimizations
|
||||
const enhancedThumbStyles = {
|
||||
...thumbStyles,
|
||||
performance: [
|
||||
'will-change-transform',
|
||||
'transform translate3d(0,0,0)',
|
||||
'backface-visibility-hidden',
|
||||
],
|
||||
};
|
||||
|
||||
const resetItem = useCallback(() => {
|
||||
handleChange(Number(props.default));
|
||||
toast(variables.getMessage('toasts.reset'));
|
||||
};
|
||||
}, [props.default, handleChange]);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<span className={'sliderTitle'}>
|
||||
{this.props.title}
|
||||
<span>{Number(this.state.value)}</span>
|
||||
<span className="link" onClick={this.resetItem}>
|
||||
<MdRefresh />
|
||||
{variables.getMessage('modals.main.settings.buttons.reset')}
|
||||
</span>
|
||||
</span>
|
||||
<Slider
|
||||
value={Number(this.state.value)}
|
||||
onChange={this.handleChange}
|
||||
valueLabelDisplay="auto"
|
||||
default={Number(this.props.default)}
|
||||
min={Number(this.props.min)}
|
||||
max={Number(this.props.max)}
|
||||
step={Number(this.props.step) || 1}
|
||||
getAriaValueText={(value) => `${value}`}
|
||||
marks={this.props.marks || []}
|
||||
const handleMarkClick = useCallback(
|
||||
(markValue) => {
|
||||
handleChange(markValue);
|
||||
},
|
||||
[handleChange],
|
||||
);
|
||||
|
||||
// Memoized render functions to prevent unnecessary re-renders
|
||||
const renderThumb = useMemo(
|
||||
() => (thumbProps, state) => (
|
||||
<div
|
||||
{...thumbProps}
|
||||
style={{
|
||||
...thumbProps.style,
|
||||
left: thumbProps.style.left,
|
||||
marginLeft: 0,
|
||||
transform: 'translateY(-50%)', // Only handle Y transform here
|
||||
}}
|
||||
aria-label={`Slider value: ${state.valueNow}%`}
|
||||
aria-valuenow={`${state.valueNow}${props.display}`}
|
||||
role="slider"
|
||||
aria-valuemin={state.valueMin}
|
||||
aria-valuemax={state.valueMax}
|
||||
/>
|
||||
),
|
||||
[props.display],
|
||||
);
|
||||
|
||||
const renderMark = useMemo(
|
||||
() => (markProps) => {
|
||||
if (!MULTIPLIER_MARKS[markProps.key]) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...markProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleMarkClick(markProps.key);
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
data-value={MULTIPLIER_MARKS[markProps.key]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Set zoom to ${MULTIPLIER_MARKS[markProps.key]}`}
|
||||
className={`${markProps.className} cursor-pointer`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleMarkClick(markProps.key);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
[handleMarkClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-white">{props.title}</span>
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<ValueDisplay
|
||||
value={value}
|
||||
display={props.display}
|
||||
onChange={handleChange}
|
||||
min={Number(props.min)}
|
||||
max={Number(props.max)}
|
||||
/>
|
||||
<ResetButton onClick={resetItem} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReactSlider
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBeforeChange={handleDragStart}
|
||||
onAfterChange={handleDragEnd}
|
||||
min={Number(props.min)}
|
||||
max={Number(props.max)}
|
||||
step={Number(props.step) || 1}
|
||||
marks={Object.keys(MULTIPLIER_MARKS).map(Number)}
|
||||
snapPoints={Object.keys(MULTIPLIER_MARKS).map(Number)}
|
||||
snap
|
||||
className="h-14 flex items-center touch-none"
|
||||
thumbClassName={clsx([
|
||||
...thumbStyles.base,
|
||||
thumbStyles.colors,
|
||||
...thumbStyles.interactions,
|
||||
thumbStyles.effects,
|
||||
...thumbStyles.tooltip,
|
||||
'touch-none',
|
||||
'left-0', // Add this to ensure proper positioning
|
||||
])}
|
||||
trackClassName="h-2 bg-neutral-300 dark:bg-white/15 rounded-full absolute top-1/2 transform -translate-y-1/2 touch-none"
|
||||
markClassName={clsx([
|
||||
...markStyles.base,
|
||||
markStyles.colors,
|
||||
markStyles.hover,
|
||||
markStyles.transitions,
|
||||
...markStyles.label,
|
||||
])}
|
||||
renderThumb={renderThumb}
|
||||
renderMark={renderMark}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SliderComponent as default, SliderComponent as Slider };
|
||||
// Export memoized version for better performance when parent components re-render
|
||||
const Slider = memo(SliderComponent);
|
||||
Slider.displayName = 'Slider';
|
||||
|
||||
export { Slider as default, Slider };
|
||||
|
||||
@@ -1,60 +1,53 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { Switch as SwitchUI, FormControlLabel } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { Field, Label, Switch as SwitchUI } from '@headlessui/react';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class Switch extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
checked: localStorage.getItem(this.props.name) === 'true',
|
||||
};
|
||||
}
|
||||
function Switch(props) {
|
||||
const [checked, setChecked] = useState(localStorage.getItem(props.name) === 'true');
|
||||
|
||||
handleChange = () => {
|
||||
const value = this.state.checked !== true;
|
||||
localStorage.setItem(this.props.name, value);
|
||||
const handleChange = () => {
|
||||
const value = checked !== true;
|
||||
localStorage.setItem(props.name, value);
|
||||
|
||||
this.setState({
|
||||
checked: value,
|
||||
});
|
||||
setChecked(value);
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
if (props.onChange) {
|
||||
props.onChange(value);
|
||||
}
|
||||
|
||||
variables.stats.postEvent(
|
||||
'setting',
|
||||
`${this.props.name} ${this.state.checked === true ? 'enabled' : 'disabled'}`,
|
||||
`${props.name} ${checked === true ? 'enabled' : 'disabled'}`,
|
||||
);
|
||||
|
||||
if (this.props.element) {
|
||||
if (!document.querySelector(this.props.element)) {
|
||||
if (props.element) {
|
||||
if (!document.querySelector(props.element)) {
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
return localStorage.setItem('showReminder', true);
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.emit('refresh', this.props.category);
|
||||
EventBus.emit('refresh', props.category);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<SwitchUI
|
||||
name={this.props.name}
|
||||
color="primary"
|
||||
checked={this.state.checked}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
}
|
||||
label={this.props.header ? '' : this.props.text}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Field className="flex flex-row items-center justify-between w-[100%]">
|
||||
<Label>{props.header ? '' : props.text}</Label>
|
||||
<SwitchUI
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
className="box-border group relative flex h-7 w-14 cursor-pointer rounded-full bg-white/10 p-1 transition-colors duration-200 ease-in-out focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white data-[checked]:bg-white/10"
|
||||
>
|
||||
{' '}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none inline-block size-5 translate-x-0 rounded-full bg-white ring-0 shadow-lg transition duration-200 ease-in-out group-data-[checked]:translate-x-7"
|
||||
/>
|
||||
</SwitchUI>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch as default, Switch };
|
||||
|
||||