mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
Compare commits
511 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fda0071251 | |||
| 8a5100c06f | |||
| cf2778951f | |||
| 6fa1af2f68 | |||
| b8286b6f22 | |||
| 8466ef7691 | |||
| d75959eeb1 | |||
| 94a2f4ac34 | |||
| 1647870186 | |||
| b332de52ff | |||
| daf7ea8e83 | |||
| 341087b6a0 | |||
| a0d8e05fd0 | |||
| 399f68c547 | |||
| 3ddf1d0c4f | |||
| 10a6c458b1 | |||
| 33825843b7 | |||
| 56dabc8fd5 | |||
| 0c1a71f398 | |||
| bb6ee72159 | |||
| d52a59ae48 | |||
| 1c9e361f78 | |||
| ec3396c52e | |||
| 5b5dba69dc | |||
| 449b54ae32 | |||
| c29dc45697 | |||
| d4127626b1 | |||
| 907f970018 | |||
| d9b1482255 | |||
| 454ab283ab | |||
| 0ef43eb9b5 | |||
| ecbdffbbde | |||
| 92344400e1 | |||
| ca20ba4e07 | |||
| 694d4ea0a1 | |||
| 72a529ee1d | |||
| 0a3ee5c666 | |||
| ef6176b6a4 | |||
| b3c395cca1 | |||
| 8c2539f130 | |||
| 442ea04a2f | |||
| bd812ffdae | |||
| 6377a0c909 | |||
| d8829d5716 | |||
| 7fd85a5529 | |||
| 9562368157 | |||
| ab867af57d | |||
| 886d0a95f1 | |||
| dd47deb954 | |||
| fbf066cea8 | |||
| eb2c665843 | |||
| 45a16de405 | |||
| 048ccb248e | |||
| 363fbfa3c8 | |||
| 0bf4ed8157 | |||
| 814647e835 | |||
| 07aa9524aa | |||
| 13f830ee16 | |||
| 1b4708261d | |||
| 6a556b6940 | |||
| d0edad8134 | |||
| 5e93ae6e4b | |||
| 0788b78e73 | |||
| e884b0526b | |||
| ea77224c75 | |||
| 18441712c9 | |||
| 3dc77dd398 | |||
| e7c5357c64 | |||
| 8df138a374 | |||
| 068e4ab778 | |||
| adbba730c4 | |||
| 1f3354c47b | |||
| 7a80dc2cc3 | |||
| 68e8c89b35 | |||
| 77582a4d00 | |||
| 3f97049451 | |||
| ebc7baaacc | |||
| 35ca292c04 | |||
| e928399066 | |||
| a4033862c9 | |||
| 22ddb4bc41 | |||
| b8d8b108c3 | |||
| aeaf5d9e59 | |||
| 1acda4f399 | |||
| 121888c1c3 | |||
| 647a32fbac | |||
| 19cc1a5600 | |||
| e3f4b59d9c | |||
| a07323499c | |||
| 600456f28e | |||
| 3ecd7205ed | |||
| 6147e96cc9 | |||
| 09855c9ef5 | |||
| 9542cb13f5 | |||
| d19f573093 | |||
| 7af6acaf38 | |||
| c4c50f2c30 | |||
| a33f4f3f00 | |||
| 1f023574b8 | |||
| dc4499e8a2 | |||
| ad2ad4d456 | |||
| 5413286f56 | |||
| f0c5b1dace | |||
| ad14dc3aa5 | |||
| 64bf1d88e8 | |||
| 7196a85f7d | |||
| f2b594a13b | |||
| a17a9a50c1 | |||
| 207832640f | |||
| b76999cb13 | |||
| fc0e491ea7 | |||
| 68159ddd0e | |||
| 4696529964 | |||
| a9e198ea68 | |||
| 620d168d28 | |||
| 1c63c06b72 | |||
| 7a76d3f4eb | |||
| 8e34db4a67 | |||
| 9fc24767ec | |||
| 331c9a9d81 | |||
| 74e92ddb53 | |||
| 1a6dc9ebb9 | |||
| be54816d83 | |||
| b644dbbbc7 | |||
| d06356101a | |||
| 7eacf345d0 | |||
| 9a71a5241a | |||
| f4ae9098d8 | |||
| 325f6c5f9b | |||
| ea46ab41ce | |||
| e6f36edabf | |||
| 587aa5eb89 | |||
| da3a680455 | |||
| 77c3761947 | |||
| 6fb4ea5372 | |||
| 5c0044a4d4 | |||
| dba688d3cd | |||
| 75446c6855 | |||
| fe2fa87cb5 | |||
| 9f7b46d2ad | |||
| ef890ee776 | |||
| d42dc79415 | |||
| e072b3f5c8 | |||
| e32218bf07 | |||
| 286375c662 | |||
| f2d197e8f0 | |||
| 85beb62a37 | |||
| 0b908cb251 | |||
| c9f0f9cf16 | |||
| 3c65e6d6c5 | |||
| 2cb607c5a9 | |||
| 695357a639 | |||
| 8cb052f2ff | |||
| 6b39f60db7 | |||
| 1638dd4989 | |||
| ca7e6b9137 | |||
| 1263c1c8ef | |||
| 5eb92bc87a | |||
| ecff10a991 | |||
| 4745df7ace | |||
| c7bdd86967 | |||
| f920980948 | |||
| 8c2f36033f | |||
| 75e687f934 | |||
| 5cd0f47fe5 | |||
| 84cfaccded | |||
| 0c55098bc7 | |||
| 50157f24fd | |||
| b77e2b2247 | |||
| 0c0fabe661 | |||
| f39bfce5c3 | |||
| 2d26f729e3 | |||
| d7b541c814 | |||
| 41bb5996df | |||
| d3d7a1199f | |||
| 3277b02dfb | |||
| 4703d68bac | |||
| 696043e01a | |||
| 24ef85c39e | |||
| 35fc996e37 | |||
| edb0a0f929 | |||
| 5051d04451 | |||
| ffc695f022 | |||
| ca5d232e47 | |||
| 44325f0d49 | |||
| c446217916 | |||
| a51049154b | |||
| f41da95f7e | |||
| 3e405cc453 | |||
| d3ae21b7fa | |||
| 6247e17d70 | |||
| 550f2cab54 | |||
| ddb94e6b07 | |||
| 12270d28b9 | |||
| 639d35b2f5 | |||
| 410bd0e54e | |||
| c1bc3d3d22 | |||
| 17b093b5ea | |||
| c8330091ca | |||
| 2a00344243 | |||
| cd2c98bd65 | |||
| 81b690ec9a | |||
| 13095cef19 | |||
| 36ecbd37ed | |||
| 2cc5ce3f1a | |||
| 14aa511198 | |||
| 4e397e3c57 | |||
| af311d9b3e | |||
| 3af28f574b | |||
| 9f1c3e3bc8 | |||
| e7df2abc6d | |||
| c7ae2e1ab6 | |||
| a0888eb091 | |||
| e8d9dc7a6b | |||
| 32934593d8 | |||
| 855d979b7f | |||
| 083dfad5c2 | |||
| 9de863be02 | |||
| 9d7dab84f1 | |||
| a6999051c4 | |||
| c35855559b | |||
| 8972a5a8bf | |||
| a321a482cc | |||
| 5f561f516c | |||
| 395ec3291e | |||
| 96b17c7eeb | |||
| fad50e6eba | |||
| f74ad97c0a | |||
| 7f4e6cf5ec | |||
| 677f17c418 | |||
| e58584a55a | |||
| 59444dc904 | |||
| 178c4fdef4 | |||
| cdaaceade7 | |||
| d65bfa8c46 | |||
| 694d11477d | |||
| 61e1bcdae9 | |||
| 23a09004d8 | |||
| 3ce075cd47 | |||
| 92a51daf36 | |||
| 479b2878a9 | |||
| 18ffa1b47d | |||
| 6098cf9608 | |||
| 5fde2a3660 | |||
| e4d5f7fd3f | |||
| 31b069056d | |||
| 3e5ebe8ef4 | |||
| 338292ac15 | |||
| 187c484901 | |||
| 24d0616110 | |||
| 260ac4aaea | |||
| 4311a8fe76 | |||
| 251e09941b | |||
| bb1541ab2d | |||
| 1c6ec3ee91 | |||
| 0ef0078fb7 | |||
| 834b8b41af | |||
| f1512ba6e1 | |||
| 13fc077686 | |||
| 7cf765121c | |||
| 4e393f14bb | |||
| 98347e038d | |||
| f2bdb22ea8 | |||
| 4afab2c52a | |||
| 4c6b43d7c7 | |||
| 9e26d2c192 | |||
| 7445e8be78 | |||
| d1a876ff22 | |||
| e2176ea2fa | |||
| a999e4384b | |||
| 4bf5420140 | |||
| 8fb29f7f21 | |||
| 44e3ed34d0 | |||
| 32228ee4db | |||
| 1692bd3e92 | |||
| 71cf9dbca8 | |||
| 372b591b16 | |||
| 7578ecee74 | |||
| a2b4f81b86 | |||
| fcd95f6823 | |||
| f2ea7c8104 | |||
| 379a3ebda0 | |||
| d1850e8ddb | |||
| 88a87692cd | |||
| 4e6e4870b0 | |||
| 18f215fa5f | |||
| 2ea8ada439 | |||
| 34b2501617 | |||
| 88d4d3aa11 | |||
| 5ed3a05f6a | |||
| 430f158957 | |||
| 547caabc45 | |||
| f6e549c5da | |||
| dc1ae9c0a1 | |||
| 34306e77cf | |||
| 00c9f03827 | |||
| 6c93477998 | |||
| 9784b6162f | |||
| 96cf8e3eac | |||
| 0e23ea0cc3 | |||
| 7dfe347562 | |||
| 21a8472c94 | |||
| 8e11ab821b | |||
| cca59ebf06 | |||
| 14648b70f5 | |||
| 0184e90088 | |||
| aa977a259d | |||
| 1507e9cdfe | |||
| c816174aed | |||
| 85dee0e2c1 | |||
| bebe0ac9b2 | |||
| f4e8c68ef3 | |||
| 8d1168d6c4 | |||
| 5dc3526711 | |||
| 0bb4d89570 | |||
| e5c05c0dca | |||
| 172021d0d0 | |||
| f9f7b54adc | |||
| 44126b6ee6 | |||
| 3ca9cf4415 | |||
| be1336a9ec | |||
| 34185e61ff | |||
| 426e0749ef | |||
| bb6de3a1a2 | |||
| 9de6e8feaf | |||
| fe82365c24 | |||
| 60e7dad261 | |||
| f8caf9cb35 | |||
| 1a17f91f10 | |||
| 043c466d9e | |||
| 3d458a185e | |||
| 58d3172c5d | |||
| ab3d4b212c | |||
| 68c94f80d6 | |||
| 2627204112 | |||
| 54e7b58794 | |||
| 3d276e3b22 | |||
| 0671a7370b | |||
| 4a7cd9b7a1 | |||
| f2b299cc9c | |||
| c1e1741b71 | |||
| bfb253341e | |||
| d91d67cf15 | |||
| 8b672f3e67 | |||
| bf1f7bfb3b | |||
| 34c88b06e8 | |||
| 528b8b49a1 | |||
| 1ad1a758fa | |||
| cdfa6723ef | |||
| 052c06a04d | |||
| 75f5f698da | |||
| 87b374571e | |||
| 1fe4c71656 | |||
| b45b5cb22f | |||
| e55e4c8a06 | |||
| a5098d62ed | |||
| f5462495c1 | |||
| b70a3e9268 | |||
| cc6091c899 | |||
| 6153861f54 | |||
| a8987b5a7b | |||
| e1544b1ee5 | |||
| b404828f0e | |||
| 83b0e8de3f | |||
| 922a3d5837 | |||
| 386445c7ee | |||
| 7555b0ff14 | |||
| 6597ff9075 | |||
| 84eaecdadd | |||
| 818ff48a0d | |||
| 33e34a0552 | |||
| d4a1fe199a | |||
| 2eefeb30b6 | |||
| e55fb35bf9 | |||
| 008666d81d | |||
| 8b26947fcf | |||
| d7a38a273c | |||
| a140bf02e0 | |||
| d624e9df32 | |||
| 881339d016 | |||
| 096c53b359 | |||
| 3440e86e2e | |||
| 43ff5d1037 | |||
| 0106124a60 | |||
| caa92e1f67 | |||
| 278a085286 | |||
| 2d325f820d | |||
| 53cfb27899 | |||
| b6e91b2254 | |||
| 8b0de97bc6 | |||
| 49d4f39584 | |||
| 1360736dd7 | |||
| e49849c18a | |||
| 6267a77a71 | |||
| 548dcbf34e | |||
| 92c0076e2d | |||
| 91ec33c0f9 | |||
| 384663912d | |||
| a0082dc895 | |||
| 03ffe22fbb | |||
| 8f5013d2ff | |||
| 831853798e | |||
| 90a4c8f048 | |||
| c9550d0d37 | |||
| 2a9e901b2b | |||
| f65dc92490 | |||
| fea486aa52 | |||
| 31e833f791 | |||
| d0a7749006 | |||
| 94684a9481 | |||
| 580cd9b3d9 | |||
| ae10f334f1 | |||
| 3b62a82d91 | |||
| 92fd20c380 | |||
| 0b9240a390 | |||
| 6885ae2d08 | |||
| f9c3fbcc87 | |||
| b8c99baf0c | |||
| 31cd9d0e48 | |||
| f5119ac9ca | |||
| 22d1f50372 | |||
| 3819fb39c8 | |||
| 3388281744 | |||
| 1272c60a4d | |||
| 3e851b335b | |||
| 8e7782b6a1 | |||
| 263a7f3cda | |||
| 17228e0444 | |||
| 3cd2f4ede8 | |||
| e55fd65590 | |||
| 1ff14b8f3e | |||
| db120c00ce | |||
| 1aa63274e7 | |||
| 08c07db6cc | |||
| 8d05692a9b | |||
| 538b46dac4 | |||
| c727bc668b | |||
| fec555d220 | |||
| 02a1e8e32d | |||
| 4187a9cade | |||
| 8d08354f6c | |||
| 15c6283bcf | |||
| de1b0c3194 | |||
| 6da4c791a6 | |||
| d1466da58b | |||
| 01fc068bcd | |||
| f08b851846 | |||
| 10e8bc29df | |||
| 40650f902d | |||
| 99c342da85 | |||
| 5f6e0fa122 | |||
| ad8eb2a273 | |||
| df23c4f888 | |||
| ed6757edb1 | |||
| f1ba486dbf | |||
| 531c350f06 | |||
| 2a91e7056e | |||
| a04d8211ed | |||
| 9f8816f322 | |||
| b55e8b47a0 | |||
| e8c0f075ec | |||
| b4ca961392 | |||
| 7fbdf8bf32 | |||
| 8ccbdd49e1 | |||
| 64ffc462d0 | |||
| f985f9445f | |||
| 966b58b932 | |||
| 2348b90023 | |||
| d68ba1521a | |||
| f6a58cda0f | |||
| 7822d210b2 | |||
| 2d23669aa3 | |||
| 7951358cd0 | |||
| 7ca4682adb | |||
| d04965db6a | |||
| ae1b676fc3 | |||
| 6b20c13705 | |||
| 38ddcbf5ca | |||
| bff4a2abf4 | |||
| 54d7eae95b | |||
| fdeea2f626 | |||
| 272deb2b8c | |||
| c3cb2937c9 | |||
| f0bdbbb14f | |||
| 52c8809cf2 | |||
| f95b845b92 | |||
| a82cd79954 | |||
| cd430f2027 | |||
| 573ac401be | |||
| ac73056128 | |||
| 9363de5fb4 | |||
| 7f93aef9cc | |||
| 220d15ebbc | |||
| 428ad7569e | |||
| 4a8ed32d3e | |||
| e001078808 | |||
| cf379c61aa | |||
| e3ec0d83ab | |||
| c2da4c1ed5 | |||
| d567c625c4 | |||
| 52bbe4d7e4 | |||
| 89f2743475 | |||
| 19102f9bcd | |||
| c008b32efa | |||
| c376183082 | |||
| e4ba89073c | |||
| 2f08d6ee08 | |||
| 99a3166fa4 | |||
| 59a8084e98 | |||
| 0d0e526a25 | |||
| 125ebfbaea |
@@ -0,0 +1,414 @@
|
|||||||
|
/** @type {import('dependency-cruiser').IConfiguration} */
|
||||||
|
module.exports = {
|
||||||
|
forbidden: [
|
||||||
|
{
|
||||||
|
name: 'no-circular',
|
||||||
|
severity: 'warn',
|
||||||
|
comment:
|
||||||
|
'This dependency is part of a circular relationship. You might want to revise ' +
|
||||||
|
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
circular: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'no-orphans',
|
||||||
|
comment:
|
||||||
|
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
|
||||||
|
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
|
||||||
|
"add an exception for it in your dependency-cruiser configuration. By default " +
|
||||||
|
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
|
||||||
|
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
|
||||||
|
severity: 'warn',
|
||||||
|
from: {
|
||||||
|
orphan: true,
|
||||||
|
pathNot: [
|
||||||
|
'(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files
|
||||||
|
'[.]d[.]ts$', // TypeScript declaration files
|
||||||
|
'(^|/)tsconfig[.]json$', // TypeScript config
|
||||||
|
'(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs
|
||||||
|
]
|
||||||
|
},
|
||||||
|
to: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'no-deprecated-core',
|
||||||
|
comment:
|
||||||
|
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
|
||||||
|
"bound to exist - node doesn't deprecate lightly.",
|
||||||
|
severity: 'warn',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
dependencyTypes: [
|
||||||
|
'core'
|
||||||
|
],
|
||||||
|
path: [
|
||||||
|
'^v8/tools/codemap$',
|
||||||
|
'^v8/tools/consarray$',
|
||||||
|
'^v8/tools/csvparser$',
|
||||||
|
'^v8/tools/logreader$',
|
||||||
|
'^v8/tools/profile_view$',
|
||||||
|
'^v8/tools/profile$',
|
||||||
|
'^v8/tools/SourceMap$',
|
||||||
|
'^v8/tools/splaytree$',
|
||||||
|
'^v8/tools/tickprocessor-driver$',
|
||||||
|
'^v8/tools/tickprocessor$',
|
||||||
|
'^node-inspect/lib/_inspect$',
|
||||||
|
'^node-inspect/lib/internal/inspect_client$',
|
||||||
|
'^node-inspect/lib/internal/inspect_repl$',
|
||||||
|
'^async_hooks$',
|
||||||
|
'^punycode$',
|
||||||
|
'^domain$',
|
||||||
|
'^constants$',
|
||||||
|
'^sys$',
|
||||||
|
'^_linklist$',
|
||||||
|
'^_stream_wrap$'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'not-to-deprecated',
|
||||||
|
comment:
|
||||||
|
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
|
||||||
|
'version of that module, or find an alternative. Deprecated modules are a security risk.',
|
||||||
|
severity: 'warn',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
dependencyTypes: [
|
||||||
|
'deprecated'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'no-non-package-json',
|
||||||
|
severity: 'error',
|
||||||
|
comment:
|
||||||
|
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
|
||||||
|
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
|
||||||
|
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
|
||||||
|
"in your package.json.",
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
dependencyTypes: [
|
||||||
|
'npm-no-pkg',
|
||||||
|
'npm-unknown'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'not-to-unresolvable',
|
||||||
|
comment:
|
||||||
|
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
|
||||||
|
'module: add it to your package.json. In all other cases you likely already know what to do.',
|
||||||
|
severity: 'error',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
couldNotResolve: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'no-duplicate-dep-types',
|
||||||
|
comment:
|
||||||
|
"Likely this module depends on an external ('npm') package that occurs more than once " +
|
||||||
|
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
|
||||||
|
"maintenance problems later on.",
|
||||||
|
severity: 'warn',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
moreThanOneDependencyType: true,
|
||||||
|
// as it's pretty common to have a type import be a type only import
|
||||||
|
// _and_ (e.g.) a devDependency - don't consider type-only dependency
|
||||||
|
// types for this rule
|
||||||
|
dependencyTypesNot: ["type-only"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* rules you might want to tweak for your specific situation: */
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'not-to-spec',
|
||||||
|
comment:
|
||||||
|
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
|
||||||
|
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
|
||||||
|
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
|
||||||
|
severity: 'error',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'not-to-dev-dep',
|
||||||
|
severity: 'error',
|
||||||
|
comment:
|
||||||
|
"This module depends on an npm package from the 'devDependencies' section of your " +
|
||||||
|
'package.json. It looks like something that ships to production, though. To prevent problems ' +
|
||||||
|
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
|
||||||
|
'section of your package.json. If this module is development only - add it to the ' +
|
||||||
|
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
|
||||||
|
from: {
|
||||||
|
path: '^(src)',
|
||||||
|
pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
dependencyTypes: [
|
||||||
|
'npm-dev',
|
||||||
|
],
|
||||||
|
// type only dependencies are not a problem as they don't end up in the
|
||||||
|
// production code or are ignored by the runtime.
|
||||||
|
dependencyTypesNot: [
|
||||||
|
'type-only'
|
||||||
|
],
|
||||||
|
pathNot: [
|
||||||
|
'node_modules/@types/'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'optional-deps-used',
|
||||||
|
severity: 'info',
|
||||||
|
comment:
|
||||||
|
"This module depends on an npm package that is declared as an optional dependency " +
|
||||||
|
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
|
||||||
|
"If you're using an optional dependency here by design - add an exception to your" +
|
||||||
|
"dependency-cruiser configuration.",
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
dependencyTypes: [
|
||||||
|
'npm-optional'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'peer-deps-used',
|
||||||
|
comment:
|
||||||
|
"This module depends on an npm package that is declared as a peer dependency " +
|
||||||
|
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
|
||||||
|
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
|
||||||
|
"add an exception to your dependency-cruiser configuration.",
|
||||||
|
severity: 'warn',
|
||||||
|
from: {},
|
||||||
|
to: {
|
||||||
|
dependencyTypes: [
|
||||||
|
'npm-peer'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
|
||||||
|
/* Which modules not to follow further when encountered */
|
||||||
|
doNotFollow: {
|
||||||
|
/* path: an array of regular expressions in strings to match against */
|
||||||
|
path: ['node_modules']
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Which modules to exclude */
|
||||||
|
// exclude : {
|
||||||
|
// /* path: an array of regular expressions in strings to match against */
|
||||||
|
// path: '',
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Which modules to exclusively include (array of regular expressions in strings)
|
||||||
|
dependency-cruiser will skip everything not matching this pattern
|
||||||
|
*/
|
||||||
|
// includeOnly : [''],
|
||||||
|
|
||||||
|
/* List of module systems to cruise.
|
||||||
|
When left out dependency-cruiser will fall back to the list of _all_
|
||||||
|
module systems it knows of. It's the default because it's the safe option
|
||||||
|
It might come at a performance penalty, though.
|
||||||
|
moduleSystems: ['amd', 'cjs', 'es6', 'tsd']
|
||||||
|
|
||||||
|
As in practice only commonjs ('cjs') and ecmascript modules ('es6')
|
||||||
|
are widely used, you can limit the moduleSystems to those.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// moduleSystems: ['cjs', 'es6'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
false: don't look at JSDoc imports (the default)
|
||||||
|
true: dependency-cruiser will detect dependencies in JSDoc-style
|
||||||
|
import statements. Implies "parser": "tsc", so the dependency-cruiser
|
||||||
|
will use the typescript parser for JavaScript files.
|
||||||
|
|
||||||
|
For this to work the typescript compiler will need to be installed in the
|
||||||
|
same spot as you're running dependency-cruiser from.
|
||||||
|
*/
|
||||||
|
// detectJSDocImports: true,
|
||||||
|
|
||||||
|
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/'
|
||||||
|
to open it on your online repo or `vscode://file/${process.cwd()}/` to
|
||||||
|
open it in visual studio code),
|
||||||
|
*/
|
||||||
|
// prefix: `vscode://file/${process.cwd()}/`,
|
||||||
|
|
||||||
|
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
|
||||||
|
true: also detect dependencies that only exist before typescript-to-javascript compilation
|
||||||
|
"specify": for each dependency identify whether it only exists before compilation or also after
|
||||||
|
*/
|
||||||
|
tsPreCompilationDeps: true,
|
||||||
|
|
||||||
|
/* list of extensions to scan that aren't javascript or compile-to-javascript.
|
||||||
|
Empty by default. Only put extensions in here that you want to take into
|
||||||
|
account that are _not_ parsable.
|
||||||
|
*/
|
||||||
|
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
|
||||||
|
|
||||||
|
/* if true combines the package.jsons found from the module up to the base
|
||||||
|
folder the cruise is initiated from. Useful for how (some) mono-repos
|
||||||
|
manage dependencies & dependency definitions.
|
||||||
|
*/
|
||||||
|
// combinedDependencies: false,
|
||||||
|
|
||||||
|
/* if true leave symlinks untouched, otherwise use the realpath */
|
||||||
|
// preserveSymlinks: false,
|
||||||
|
|
||||||
|
/* TypeScript project file ('tsconfig.json') to use for
|
||||||
|
(1) compilation and
|
||||||
|
(2) resolution (e.g. with the paths property)
|
||||||
|
|
||||||
|
The (optional) fileName attribute specifies which file to take (relative to
|
||||||
|
dependency-cruiser's current working directory). When not provided
|
||||||
|
defaults to './tsconfig.json'.
|
||||||
|
*/
|
||||||
|
tsConfig: {
|
||||||
|
fileName: 'tsconfig.json'
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Webpack configuration to use to get resolve options from.
|
||||||
|
|
||||||
|
The (optional) fileName attribute specifies which file to take (relative
|
||||||
|
to dependency-cruiser's current working directory. When not provided defaults
|
||||||
|
to './webpack.conf.js'.
|
||||||
|
|
||||||
|
The (optional) `env` and `arguments` attributes contain the parameters
|
||||||
|
to be passed if your webpack config is a function and takes them (see
|
||||||
|
webpack documentation for details)
|
||||||
|
*/
|
||||||
|
// webpackConfig: {
|
||||||
|
// fileName: 'webpack.config.js',
|
||||||
|
// env: {},
|
||||||
|
// arguments: {}
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
|
||||||
|
for compilation
|
||||||
|
*/
|
||||||
|
// babelConfig: {
|
||||||
|
// fileName: '.babelrc',
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* List of strings you have in use in addition to cjs/ es6 requires
|
||||||
|
& imports to declare module dependencies. Use this e.g. if you've
|
||||||
|
re-declared require, use a require-wrapper or use window.require as
|
||||||
|
a hack.
|
||||||
|
*/
|
||||||
|
// exoticRequireStrings: [],
|
||||||
|
|
||||||
|
/* options to pass on to enhanced-resolve, the package dependency-cruiser
|
||||||
|
uses to resolve module references to disk. The values below should be
|
||||||
|
suitable for most situations
|
||||||
|
|
||||||
|
If you use webpack: you can also set these in webpack.conf.js. The set
|
||||||
|
there will override the ones specified here.
|
||||||
|
*/
|
||||||
|
enhancedResolveOptions: {
|
||||||
|
/* What to consider as an 'exports' field in package.jsons */
|
||||||
|
exportsFields: ["exports"],
|
||||||
|
/* List of conditions to check for in the exports field.
|
||||||
|
Only works when the 'exportsFields' array is non-empty.
|
||||||
|
*/
|
||||||
|
conditionNames: ["import", "require", "node", "default", "types"],
|
||||||
|
/* The extensions, by default are the same as the ones dependency-cruiser
|
||||||
|
can access (run `npx depcruise --info` to see which ones that are in
|
||||||
|
_your_ environment). If that list is larger than you need you can pass
|
||||||
|
the extensions you actually use (e.g. [".js", ".jsx"]). This can speed
|
||||||
|
up module resolution, which is the most expensive step.
|
||||||
|
*/
|
||||||
|
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
|
||||||
|
/* What to consider a 'main' field in package.json */
|
||||||
|
mainFields: ["module", "main", "types", "typings"],
|
||||||
|
/* A list of alias fields in package.jsons
|
||||||
|
|
||||||
|
See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and
|
||||||
|
the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields)
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
Defaults to an empty array (= don't use alias fields).
|
||||||
|
*/
|
||||||
|
// aliasFields: ["browser"],
|
||||||
|
},
|
||||||
|
|
||||||
|
/* skipAnalysisNotInRules will make dependency-cruiser execute
|
||||||
|
analysis strictly necessary for checking the rule set only.
|
||||||
|
|
||||||
|
See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules
|
||||||
|
for details
|
||||||
|
*/
|
||||||
|
skipAnalysisNotInRules: true,
|
||||||
|
|
||||||
|
/* List of built-in modules to use on top of the ones node declares.
|
||||||
|
|
||||||
|
See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#builtinmodules-influencing-what-to-consider-built-in--core-modules
|
||||||
|
for details
|
||||||
|
*/
|
||||||
|
builtInModules: {
|
||||||
|
add: [
|
||||||
|
"bun",
|
||||||
|
"bun:ffi",
|
||||||
|
"bun:jsc",
|
||||||
|
"bun:sqlite",
|
||||||
|
"bun:test",
|
||||||
|
"bun:wrap",
|
||||||
|
"detect-libc",
|
||||||
|
"undici",
|
||||||
|
"ws"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
reporterOptions: {
|
||||||
|
dot: {
|
||||||
|
/* pattern of modules that can be consolidated in the detailed
|
||||||
|
graphical dependency graph. The default pattern in this configuration
|
||||||
|
collapses everything in node_modules to one folder deep so you see
|
||||||
|
the external modules, but their innards.
|
||||||
|
*/
|
||||||
|
collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)',
|
||||||
|
|
||||||
|
/* Options to tweak the appearance of your graph.See
|
||||||
|
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
|
||||||
|
for details and some examples. If you don't specify a theme
|
||||||
|
dependency-cruiser falls back to a built-in one.
|
||||||
|
*/
|
||||||
|
// theme: {
|
||||||
|
// graph: {
|
||||||
|
// /* splines: "ortho" gives straight lines, but is slow on big graphs
|
||||||
|
// splines: "true" gives bezier curves (fast, not as nice as ortho)
|
||||||
|
// */
|
||||||
|
// splines: "true"
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
archi: {
|
||||||
|
/* pattern of modules that can be consolidated in the high level
|
||||||
|
graphical dependency graph. If you use the high level graphical
|
||||||
|
dependency graph reporter (`archi`) you probably want to tweak
|
||||||
|
this collapsePattern to your situation.
|
||||||
|
*/
|
||||||
|
collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)',
|
||||||
|
|
||||||
|
/* Options to tweak the appearance of your graph. If you don't specify a
|
||||||
|
theme for 'archi' dependency-cruiser will use the one specified in the
|
||||||
|
dot section above and otherwise use the default one.
|
||||||
|
*/
|
||||||
|
// theme: { },
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"highlightFocused": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// generated: dependency-cruiser@16.10.0 on 2025-02-16T22:32:01.621Z
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Copy this file to .env.submit and fill in the values as you wish to publish
|
||||||
|
CHROME_EXTENSION_ID=
|
||||||
|
CHROME_CLIENT_ID=
|
||||||
|
CHROME_CLIENT_SECRET=
|
||||||
|
CHROME_REFRESH_TOKEN=
|
||||||
|
CHROME_PUBLISH_TARGET=
|
||||||
|
CHROME_SKIP_SUBMIT_REVIEW=
|
||||||
|
FIREFOX_EXTENSION_ID=
|
||||||
|
FIREFOX_JWT_ISSUER=
|
||||||
|
FIREFOX_JWT_SECRET=
|
||||||
|
FIREFOX_CHANNEL=
|
||||||
|
EDGE_PRODUCT_ID=
|
||||||
|
EDGE_CLIENT_ID=
|
||||||
|
EDGE_CLIENT_SECRET=
|
||||||
|
EDGE_ACCESS_TOKEN_URL=
|
||||||
|
EDGE_SKIP_SUBMIT_REVIEW= # true or false
|
||||||
+2
-2
@@ -3,12 +3,12 @@
|
|||||||
"browser": true,
|
"browser": true,
|
||||||
"commonjs": true,
|
"commonjs": true,
|
||||||
"es2021": true,
|
"es2021": true,
|
||||||
"node": true // add this line to allow Node.js-specific globals
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
"sourceType": "module" // add this line to allow 'import' and 'export' statements
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
// allow importing ts extensions
|
// allow importing ts extensions
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Report an issue with the modpack in its unmodified state. For other issues, use Discord.
|
||||||
|
labels: bug
|
||||||
|
title: "[BUG]"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Before reporting an issue, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) to make sure it has not already been reported (make sure to search closed issues as well!).
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: Describe your issue. For general issues and questions you'll get a faster answer [from our Discord.](https://discord.gg/YzmbnCDkat)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Extension version
|
||||||
|
description: What version of the extension are you using?
|
||||||
|
placeholder: Find it by opening the config menu and clicking the about icon in the top right.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Browser
|
||||||
|
description: Which Browser are you using?
|
||||||
|
options:
|
||||||
|
- Chrome
|
||||||
|
- Firefox
|
||||||
|
- Brave
|
||||||
|
- Safari
|
||||||
|
- DuckDuckGO
|
||||||
|
- Microsoft Edge
|
||||||
|
- Other Chromium-Based Browser
|
||||||
|
- Other Non-Chromium-Based Browser
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Confirm
|
||||||
|
options:
|
||||||
|
- label: This bug report is about an issue with the extension itself. I have not modified the extension nor added any unsupported plugins. If this is not the case, I know that I should post the issue to the extension's Discord support channel instead.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Screenshots, video or any other information. Include photos of the console if possible
|
||||||
|
placeholder: |
|
||||||
|
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
contact_links:
|
||||||
|
- name: BetterSEQTA Community Support
|
||||||
|
url: https://discord.gg/YzmbnCDkat
|
||||||
|
about: Join our discord for community updates, discussion, and more!
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest a new Feature to be added or replaced in BetterSeqtaPLUS
|
||||||
|
labels: enhancement
|
||||||
|
title: "[FR]"
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Confirm
|
||||||
|
options:
|
||||||
|
- label: "Is this feature request related to a Bug report?"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Bug report link
|
||||||
|
description: "If this feature request is related to a bug report, please insert the link to the bug report here"
|
||||||
|
placeholder: "https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Feature details
|
||||||
|
Before you request a feature, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) if it has already been requested. (Make sure to check closed issues as well!)
|
||||||
|
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Feature type
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- Graphical
|
||||||
|
- Functional
|
||||||
|
- Not Sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Feature Details
|
||||||
|
description: Please write, with as much detail as possible, what you would like to see from this mod.
|
||||||
|
placeholder: I would like to see...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional details
|
||||||
|
description: Anything else you want to add?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||||
|
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] This change requires a documentation update
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install --legacy-peer-deps
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Zip dist folder
|
- name: Zip dist folder
|
||||||
+5
-12
@@ -7,24 +7,17 @@ yarn.lock
|
|||||||
|
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
.env
|
.env
|
||||||
|
.env.submit
|
||||||
|
|
||||||
|
dependency-graph.svg
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
extension.zip
|
extension.zip
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
betterseqtaplus-safari/
|
||||||
|
|
||||||
.million/
|
.million/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
# Sentry Config File
|
|
||||||
.env.sentry-build-plugin
|
|
||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.env.sentry-build-plugin
|
|
||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.env.sentry-build-plugin
|
|
||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.sentryclirc
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"tailwindcss": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"semi": false
|
"semi": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,21 @@
|
|||||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||||
email, or any other method with the owners of this repository before making a change.
|
email, or any other method with the owners of this repository before making a change.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
Join our community channels to discuss the project, get help, and connect with other contributors:
|
||||||
|
|
||||||
|
- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta)
|
||||||
|
- **GitHub Discussions**: For longer-form conversations
|
||||||
|
- **GitHub Issues**: For bug reports and feature requests
|
||||||
|
|
||||||
|
## Creating Plugins
|
||||||
|
|
||||||
|
If you're interested in creating plugins for BetterSEQTA+, check out our plugin development guides:
|
||||||
|
|
||||||
|
- [Creating Your First Plugin](./docs/plugins/creating-plugins.md)
|
||||||
|
- [Plugin API Reference](./docs/advanced/plugin-api.md)
|
||||||
|
|
||||||
## Pull Request Process
|
## Pull Request Process
|
||||||
|
|
||||||
1. It is recommended to start by opening an issue to discuss the change you wish to make. This will allow us to discuss the change and ensure it is a good fit for the project.
|
1. It is recommended to start by opening an issue to discuss the change you wish to make. This will allow us to discuss the change and ensure it is a good fit for the project.
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
|
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
|
||||||
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
|
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
|
||||||
</a>
|
</a>
|
||||||
@@ -40,28 +43,19 @@
|
|||||||
- Easier Access Notices
|
- Easier Access Notices
|
||||||
- Assessments
|
- Assessments
|
||||||
- Options to remove certain items from the side menu
|
- Options to remove certain items from the side menu
|
||||||
- Fully customisable themes and an offical theme store
|
- Grades calculator
|
||||||
|
- Fully customisable themes and an official theme store
|
||||||
- Notification for next lesson (sent 5 minutes before end of the lesson)
|
- Notification for next lesson (sent 5 minutes before end of the lesson)
|
||||||
- Browser Support
|
- Browser Support
|
||||||
- Chrome Supported
|
- Chrome, Edge, Brave, Opera and other Chromium-Based browsers are supported
|
||||||
- Edge Supported
|
- Firefox Supported: [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)!
|
||||||
- Brave Supported
|
- Safari (Experimental and not recommended - only available via compilation)
|
||||||
- Opera Supported
|
|
||||||
- Vivaldi Supported
|
|
||||||
- Firefox (Experimental - available [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)
|
|
||||||
- Safari (Experimental - only available via compilation)
|
|
||||||
|
|
||||||
## Creating Custom Themes
|
## Creating Custom Themes
|
||||||
|
|
||||||
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/SethBurkart123/BetterSEQTA-theme-generator).
|
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/BetterSEQTA/BetterSEQTA-Theme-Generator).
|
||||||
|
|
||||||
Don't worry- if you get stuck feel free to ask around in the discord. We're open and happy to help out! Happy creating :)
|
Don't worry- if you get stuck feel free to ask around in the [discord](https://discord.gg/YzmbnCDkat). We're open and happy to help out! Happy creating :)
|
||||||
|
|
||||||
## Creating Custom Themes
|
|
||||||
|
|
||||||
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/SethBurkart123/BetterSEQTA-theme-generator).
|
|
||||||
|
|
||||||
Don't worry- if you get stuck feel free to ask around in the discord. We're open and happy to help out! Happy creating :)
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -71,20 +65,43 @@ Don't worry- if you get stuck feel free to ask around in the discord. We're open
|
|||||||
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
|
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Development
|
|
||||||
|
|
||||||
1. Install dependencies
|
1. Install dependencies
|
||||||
|
|
||||||
|
You may install the dependencies like below:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install # or your preferred package manager like pnpm or yarn
|
npm install # or your preferred package manager like pnpm or yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
|
But it is recommended to do it like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --legacy-peer-deps # Only NPM supported
|
||||||
|
```
|
||||||
|
### Running Development
|
||||||
2. Run the dev script (it updates as you save files)
|
2. Run the dev script (it updates as you save files)
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run dev
|
npm run dev # or use your perferred package manager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Building for production
|
||||||
|
|
||||||
|
2. Run the build script
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build # or use your perferred package manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2.1. Package it up (optional)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your perferred package manager
|
||||||
|
```
|
||||||
3. Load the extension into chrome
|
3. Load the extension into chrome
|
||||||
|
|
||||||
- Go to `chrome://extensions`
|
- Go to `chrome://extensions`
|
||||||
@@ -92,35 +109,17 @@ npm run dev
|
|||||||
- Click `Load unpacked`
|
- Click `Load unpacked`
|
||||||
- Select the `dist` folder
|
- Select the `dist` folder
|
||||||
|
|
||||||
Just remember, in order to update changes to the extension, you need to click the refresh button on the extension in `chrome://extensions` whenever anything's changed.
|
Just remember, in order to update changes to the extension if you are running in developer mode, you need to click the refresh button on the extension in `chrome://extensions` whenever anything's changed.
|
||||||
|
|
||||||
### Building for production
|
|
||||||
|
|
||||||
1. Install dependencies
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install # or your preferred package manager like pnpm or yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the build script
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Package it up (optional)
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run package # This requires 7-Zip to be installed in order to work
|
|
||||||
```
|
|
||||||
|
|
||||||
## Folder Structure
|
## Folder Structure
|
||||||
|
|
||||||
The folder structure is as follows:
|
The folder structure is as follows:
|
||||||
|
|
||||||
- The `src` folder contains source files that are compiled to the build directory.
|
- The `src` folder contains source files that are compiled to the build directory.
|
||||||
|
-
|
||||||
|
- The `src/plugins` folder contains vital loaders required for BetterSEQTA+ functionality.
|
||||||
|
|
||||||
- The `src/interface` folder contains source React files that are required for the Settings page.
|
- The `src/interface` folder contains source React & Svelte files that are required for the Settings page.
|
||||||
|
|
||||||
- The `dist` folder is where the compiled code ends up, this is the folder what you need to load into chrome as an unpacked extension for development.
|
- The `dist` folder is where the compiled code ends up, this is the folder what you need to load into chrome as an unpacked extension for development.
|
||||||
|
|
||||||
@@ -130,11 +129,11 @@ The folder structure is as follows:
|
|||||||
<img src="https://contrib.rocks/image?repo=betterseqta/betterseqta-plus" />
|
<img src="https://contrib.rocks/image?repo=betterseqta/betterseqta-plus" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/contribute.md)
|
Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/CONTRIBUTING.md)
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development by [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph)
|
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development by [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph)
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#sethburkart123/EvenBetterSEQTA&Date)
|
[](https://star-history.com/#BetterSEQTA/BetterSEQTA-Plus&Date)
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Below here is the supported versions of BetterSEQTA+. Anything older than this is not supported and contains bugs.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 3.4.3 | ✅ |
|
||||||
|
| < 3.4.3 | :x: |
|
||||||
|
|
||||||
|
`*` May not work on other devices.
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you find vulnerabilities, REPORT IT IMMEDIATELY. open the [advisories tab](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories) on the left and click the green "report a vulnerability" button or use [this quick-link](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories/new) to create a new report
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# BetterSEQTA+ Documentation
|
||||||
|
|
||||||
|
🚧 DOCS UNDER CONSTRUCTION! 🚧
|
||||||
|
|
||||||
|
Welcome to the BetterSEQTA+ documentation! This documentation will help you understand how BetterSEQTA+ works and how to extend it with plugins and new features.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
- [Project Overview](./README.md) - This file
|
||||||
|
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
|
||||||
|
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
- [Creating Your First Plugin](./plugins/README.md) - A comprehensive, beginner-friendly guide to creating plugins
|
||||||
|
- [Plugin API Reference](./plugins/api-reference.md) - Detailed technical documentation of the plugin APIs
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
BetterSEQTA+ is built around several core concepts:
|
||||||
|
|
||||||
|
1. **Plugin System**: BetterSEQTA+ uses a plugin system to extend SEQTA with new features. Plugins are self-contained pieces of code that can be enabled or disabled by the user. Check out our [plugin guide](./plugins/README.md) to learn how to create your own!
|
||||||
|
|
||||||
|
2. **Type-Safe Settings**: Each plugin can define settings that are type-safe and automatically rendered in the settings UI. The settings system uses TypeScript decorators to make it easy to define settings with proper typing.
|
||||||
|
|
||||||
|
3. **Storage API**: Plugins can use the Storage API to persist data between sessions. The Storage API is also type-safe, ensuring that plugins can only access their own data.
|
||||||
|
|
||||||
|
4. **SEQTA Integration**: BetterSEQTA+ integrates with SEQTA Learn by injecting code into the page. This allows plugins to modify the SEQTA UI and add new features.
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you need help with BetterSEQTA+, you can:
|
||||||
|
|
||||||
|
- [Open an Issue](https://github.com/SeqtaLearning/betterseqta-plus/issues) - Report bugs or request features
|
||||||
|
- [Join the Discord](https://discord.gg/YzmbnCDkat) - Chat with the community
|
||||||
|
- [Email the Maintainers](mailto:betterseqta.plus@gmail.com) - Contact the maintainers directly
|
||||||
|
|
||||||
|
## Contributing to the Documentation
|
||||||
|
|
||||||
|
We welcome contributions to the documentation! If you find something unclear or missing, please open an issue or submit a pull request.
|
||||||
|
|
||||||
|
To contribute to the documentation:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Make your changes to the documentation files
|
||||||
|
3. Submit a pull request with a clear description of your changes
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BetterSEQTA+ is licensed under the [MIT License](../LICENSE).
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# Contributing to BetterSEQTA+
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to BetterSEQTA+! This document provides guidelines and instructions for contributing to the project.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Code of Conduct](#code-of-conduct)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Setting Up Your Development Environment](#setting-up-your-development-environment)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Contributing Code](#contributing-code)
|
||||||
|
- [Branching Strategy](#branching-strategy)
|
||||||
|
- [Pull Request Process](#pull-request-process)
|
||||||
|
- [Coding Standards](#coding-standards)
|
||||||
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
|
- [Suggesting Features](#suggesting-features)
|
||||||
|
- [Writing Documentation](#writing-documentation)
|
||||||
|
- [Community](#community)
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
BetterSEQTA+ is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to adhere to our Code of Conduct, which promotes respectful and harassment-free interaction.
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- Be respectful and inclusive
|
||||||
|
- Focus on what is best for the community
|
||||||
|
- Show empathy towards other community members
|
||||||
|
- Be open to constructive feedback
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Setting Up Your Development Environment
|
||||||
|
|
||||||
|
1. **Fork the Repository**
|
||||||
|
|
||||||
|
Start by forking the BetterSEQTA+ repository to your GitHub account.
|
||||||
|
|
||||||
|
2. **Clone Your Fork**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/betterseqta-plus.git
|
||||||
|
cd betterseqta-plus
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install Dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set Up Development Environment**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Install in Chrome/Firefox**
|
||||||
|
|
||||||
|
Follow the [installation instructions](./installation.md#development-installation) to load the development version into your browser.
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
Understanding the project structure will help you navigate the codebase:
|
||||||
|
|
||||||
|
```
|
||||||
|
betterseqta-plus/
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── plugins/ # Plugin system
|
||||||
|
│ │ ├── built-in/ # Built-in plugins
|
||||||
|
│ │ ├── core/ # Plugin core functionality
|
||||||
|
│ ├── settings/ # Settings system
|
||||||
|
│ ├── utils/ # Utility functions
|
||||||
|
│ ├── extension/ # Browser extension code
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── test/ # Test files
|
||||||
|
├── dist/ # Build output (generated)
|
||||||
|
├── package.json # Project dependencies
|
||||||
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
└── README.md # Project README
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing Code
|
||||||
|
|
||||||
|
### Branching Strategy
|
||||||
|
|
||||||
|
We follow a simple branching strategy:
|
||||||
|
|
||||||
|
- `main` - The main development branch
|
||||||
|
- `feature/*` - Feature branches
|
||||||
|
- `bugfix/*` - Bug fix branches
|
||||||
|
- `docs/*` - Documentation branches
|
||||||
|
|
||||||
|
Always create a new branch for your changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/my-new-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Request Process
|
||||||
|
|
||||||
|
1. **Keep PRs Focused**
|
||||||
|
|
||||||
|
Each pull request should address a single concern. If you're working on multiple features, create separate PRs for each.
|
||||||
|
|
||||||
|
2. **Write Clear Commit Messages**
|
||||||
|
|
||||||
|
Follow the conventional commits format:
|
||||||
|
```
|
||||||
|
feat: add new feature
|
||||||
|
fix: resolve bug with timetable
|
||||||
|
docs: update installation instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update Documentation**
|
||||||
|
|
||||||
|
If your changes require documentation updates, include them in the same PR.
|
||||||
|
|
||||||
|
4. **Run Tests**
|
||||||
|
|
||||||
|
Make sure all tests pass before submitting your PR:
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Submit Your PR**
|
||||||
|
|
||||||
|
When you're ready, push your branch and create a pull request on GitHub.
|
||||||
|
|
||||||
|
6. **Code Review**
|
||||||
|
|
||||||
|
All PRs will be reviewed by maintainers. Be responsive to feedback and make requested changes.
|
||||||
|
|
||||||
|
7. **Merge**
|
||||||
|
|
||||||
|
Once approved, a maintainer will merge your PR.
|
||||||
|
|
||||||
|
### Coding Standards
|
||||||
|
|
||||||
|
We follow TypeScript best practices and have a consistent code style:
|
||||||
|
|
||||||
|
1. **Use TypeScript**
|
||||||
|
|
||||||
|
All new code should be written in TypeScript with proper typing.
|
||||||
|
|
||||||
|
2. **Follow Existing Patterns**
|
||||||
|
|
||||||
|
Match the coding style of the existing codebase.
|
||||||
|
|
||||||
|
3. **Write Tests**
|
||||||
|
|
||||||
|
Add tests for new features and bug fixes.
|
||||||
|
|
||||||
|
4. **Document Your Code**
|
||||||
|
|
||||||
|
Add comments for complex logic and JSDoc comments for functions.
|
||||||
|
|
||||||
|
5. **Use Linters**
|
||||||
|
|
||||||
|
We use ESLint and Prettier. Run them before submitting your PR:
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
If you find a bug, please report it by creating an issue on GitHub:
|
||||||
|
|
||||||
|
1. **Search Existing Issues**
|
||||||
|
|
||||||
|
Check if the bug has already been reported.
|
||||||
|
|
||||||
|
2. **Use the Bug Report Template**
|
||||||
|
|
||||||
|
Fill in all sections of the bug report template:
|
||||||
|
- Description
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected behavior
|
||||||
|
- Actual behavior
|
||||||
|
- Screenshots (if applicable)
|
||||||
|
- Environment (browser, OS, etc.)
|
||||||
|
|
||||||
|
3. **Be Specific**
|
||||||
|
|
||||||
|
The more details you provide, the easier it will be to fix the bug.
|
||||||
|
|
||||||
|
## Suggesting Features
|
||||||
|
|
||||||
|
We welcome feature suggestions! To suggest a new feature:
|
||||||
|
|
||||||
|
1. **Search Existing Suggestions**
|
||||||
|
|
||||||
|
Check if your idea has already been suggested.
|
||||||
|
|
||||||
|
2. **Use the Feature Request Template**
|
||||||
|
|
||||||
|
Fill in all sections of the feature request template:
|
||||||
|
- Description
|
||||||
|
- Use case
|
||||||
|
- Potential implementation
|
||||||
|
- Alternatives considered
|
||||||
|
|
||||||
|
3. **Be Patient**
|
||||||
|
|
||||||
|
Feature requests are evaluated based on alignment with project goals, feasibility, and maintainer bandwidth.
|
||||||
|
|
||||||
|
## Writing Documentation
|
||||||
|
|
||||||
|
Good documentation is crucial for the project. To contribute to documentation:
|
||||||
|
|
||||||
|
1. **Identify Gaps**
|
||||||
|
|
||||||
|
Look for areas where documentation is missing or unclear.
|
||||||
|
|
||||||
|
2. **Follow Documentation Style**
|
||||||
|
|
||||||
|
Maintain a consistent style and format.
|
||||||
|
|
||||||
|
3. **Use Clear Language**
|
||||||
|
|
||||||
|
Write in simple, clear English. Avoid jargon when possible.
|
||||||
|
|
||||||
|
4. **Include Examples**
|
||||||
|
|
||||||
|
Code examples and screenshots help users understand.
|
||||||
|
|
||||||
|
5. **Submit a PR**
|
||||||
|
|
||||||
|
Follow the same process as code contributions, but create a branch with a `docs/` prefix.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
Join our community channels to discuss the project, get help, and connect with other contributors:
|
||||||
|
|
||||||
|
- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta)
|
||||||
|
- **GitHub Discussions**: For longer-form conversations
|
||||||
|
- **GitHub Issues**: For bug reports and feature requests
|
||||||
|
|
||||||
|
## Creating Plugins
|
||||||
|
|
||||||
|
If you're interested in creating plugins for BetterSEQTA+, check out our plugin development guides:
|
||||||
|
|
||||||
|
- [Creating Your First Plugin](./plugins/creating-plugins.md)
|
||||||
|
- [Plugin API Reference](./advanced/plugin-api.md)
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
Contributors are recognized in several ways:
|
||||||
|
|
||||||
|
1. **CONTRIBUTORS.md**: All contributors are listed in this file
|
||||||
|
2. **Release Notes**: Significant contributions are highlighted in release notes
|
||||||
|
3. **Community Recognition**: Regular shout-outs in community channels
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have any questions about contributing, please:
|
||||||
|
|
||||||
|
1. Check the documentation
|
||||||
|
2. Ask in the Discord server
|
||||||
|
3. Open a GitHub Discussion
|
||||||
|
|
||||||
|
Thank you for contributing to BetterSEQTA+! Your efforts help make SEQTA better for students and teachers everywhere.
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# Installing BetterSEQTA+
|
||||||
|
|
||||||
|
This guide will walk you through the process of installing and setting up BetterSEQTA+ for development or usage.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, make sure you have the following installed:
|
||||||
|
|
||||||
|
- [npm](https://www.npmjs.com/) (v7 or higher) or [Bun](https://bun.sh/) (recommended)
|
||||||
|
- A modern web browser (Chrome, Firefox, Edge, etc.)
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
There are two ways to install BetterSEQTA+:
|
||||||
|
|
||||||
|
1. **For Users**: Install the browser extension
|
||||||
|
2. **For Developers**: Clone the repository and set up the development environment
|
||||||
|
|
||||||
|
## For Users: Installing the Browser Extension
|
||||||
|
|
||||||
|
BetterSEQTA+ is available as a browser extension for Chrome, Firefox, and Edge.
|
||||||
|
|
||||||
|
### Chrome/Edge
|
||||||
|
|
||||||
|
1. Visit the [Chrome Web Store page for BetterSEQTA+](https://chrome.google.com/webstore/detail/betterseqta)
|
||||||
|
2. Click the "Add to Chrome" button
|
||||||
|
3. Confirm the installation when prompted
|
||||||
|
4. The extension will be installed and ready to use
|
||||||
|
|
||||||
|
### Firefox
|
||||||
|
|
||||||
|
1. Visit the [Firefox Add-ons page for BetterSEQTA+](https://addons.mozilla.org/en-US/firefox/addon/betterseqta)
|
||||||
|
2. Click the "Add to Firefox" button
|
||||||
|
3. Confirm the installation when prompted
|
||||||
|
4. The extension will be installed and ready to use
|
||||||
|
|
||||||
|
## For Developers: Setting Up the Development Environment
|
||||||
|
|
||||||
|
If you want to develop for BetterSEQTA+ or modify the code, follow these steps:
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/SeqtaLearning/betterseqta-plus.git
|
||||||
|
cd betterseqta-plus
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --legacy-peer-deps
|
||||||
|
```
|
||||||
|
|
||||||
|
Using Bun (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Set Up Environment Variables - Only required for pushing to extension stores from the command line
|
||||||
|
|
||||||
|
Copy the example environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.submit.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit the `.env` file with your SEQTA credentials and settings.
|
||||||
|
|
||||||
|
### 4. Start the Development Server
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Using Bun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start a development server and build the extension in watch mode.
|
||||||
|
|
||||||
|
### 5. Load the Extension in Your Browser
|
||||||
|
|
||||||
|
#### Chrome/Edge
|
||||||
|
|
||||||
|
1. Open Chrome/Edge and navigate to `chrome://extensions` or `edge://extensions`
|
||||||
|
2. Enable "Developer mode" using the toggle in the top right
|
||||||
|
3. Click "Load unpacked" and select the `dist` folder in your BetterSEQTA+ directory
|
||||||
|
4. The extension should now appear in your extensions list
|
||||||
|
|
||||||
|
#### Firefox
|
||||||
|
|
||||||
|
1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`
|
||||||
|
2. Click "Load Temporary Add-on..."
|
||||||
|
3. Select the `manifest.json` file in the `dist` folder
|
||||||
|
4. The extension should now appear in your add-ons list
|
||||||
|
|
||||||
|
### 6. Test Your Changes
|
||||||
|
|
||||||
|
After making changes to the code, the development server will automatically rebuild the extension. However, you may need to reload the extension in your browser to see the changes:
|
||||||
|
|
||||||
|
1. Go to the extensions page in your browser
|
||||||
|
2. Find BetterSEQTA+ and click the reload icon
|
||||||
|
3. Refresh any SEQTA Learn pages you have open
|
||||||
|
|
||||||
|
## Troubleshooting Installation
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### "Cannot find module" errors
|
||||||
|
|
||||||
|
If you see errors about missing modules, try:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf node_modules
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Bun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf node_modules
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extension not appearing in SEQTA
|
||||||
|
|
||||||
|
Make sure:
|
||||||
|
- You're visiting a SEQTA Learn page
|
||||||
|
- The extension is enabled
|
||||||
|
- You've refreshed the page after installing the extension
|
||||||
|
|
||||||
|
#### Development build not updating
|
||||||
|
|
||||||
|
Try:
|
||||||
|
1. Stopping the development server
|
||||||
|
2. Clearing your browser cache
|
||||||
|
3. Removing the extension from your browser
|
||||||
|
4. Rebuilding the extension
|
||||||
|
5. Loading it again
|
||||||
|
|
||||||
|
## Updating BetterSEQTA+
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
|
||||||
|
Browser extensions update automatically, but you can manually check for updates:
|
||||||
|
|
||||||
|
- **Chrome/Edge**: Go to `chrome://extensions` or `edge://extensions`, enable Developer mode, and click "Update"
|
||||||
|
- **Firefox**: Go to `about:addons`, click the gear icon, and select "Check for Updates"
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
If you're working on the code, pull the latest changes and reinstall dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Bun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
bun install
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you have BetterSEQTA+ installed, you can:
|
||||||
|
|
||||||
|
- [Getting Started with Plugins](./plugins/getting-started.md)
|
||||||
|
- [Contribute to the project](../CONTRIBUTING.md)
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# Creating Plugins for BetterSEQTA+
|
||||||
|
|
||||||
|
Hey there! 👋 So you want to create a plugin for BetterSEQTA+? That's awesome! This guide will walk you through everything you need to know, from the very basics to more advanced features. Don't worry if you're new to this - we'll explain everything step by step.
|
||||||
|
|
||||||
|
## What is a Plugin?
|
||||||
|
|
||||||
|
In BetterSEQTA+, a plugin is like a mini-app that adds new features to SEQTA. Think of it as a piece of LEGO that you can snap onto SEQTA to make it do new things. For example, you could create a plugin that:
|
||||||
|
- Changes how SEQTA looks
|
||||||
|
- Adds new buttons or features
|
||||||
|
- Shows extra information on your timetable
|
||||||
|
- Collects notifications in a better way
|
||||||
|
- Really, anything you can imagine!
|
||||||
|
|
||||||
|
## Your First Plugin
|
||||||
|
|
||||||
|
Let's create a super simple plugin together. We'll make one that adds a friendly message to the SEQTA homepage. Here's what we'll need:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
|
||||||
|
const myFirstPlugin: Plugin = {
|
||||||
|
// Every plugin needs these basic details
|
||||||
|
id: 'my-first-plugin',
|
||||||
|
name: 'My First Plugin',
|
||||||
|
description: 'Adds a friendly message to SEQTA',
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
// This tells BetterSEQTA+ that users can turn our plugin on/off
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
// This is where the magic happens!
|
||||||
|
run: async (api) => {
|
||||||
|
// Wait for the homepage to load
|
||||||
|
api.seqta.onMount('.home-page', (homePage) => {
|
||||||
|
// Create our message
|
||||||
|
const message = document.createElement('div');
|
||||||
|
message.textContent = 'Hello from my first plugin! 🎉';
|
||||||
|
message.style.padding = '20px';
|
||||||
|
message.style.backgroundColor = '#e9f5ff';
|
||||||
|
message.style.borderRadius = '8px';
|
||||||
|
message.style.margin = '20px';
|
||||||
|
|
||||||
|
// Add it to the page
|
||||||
|
homePage.prepend(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a cleanup function that removes our message when the plugin is disabled
|
||||||
|
return () => {
|
||||||
|
const message = document.querySelector('.home-page > div');
|
||||||
|
message?.remove();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default myFirstPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's break down what's happening here:
|
||||||
|
|
||||||
|
1. First, we import the `Plugin` type that tells TypeScript what a plugin should look like
|
||||||
|
2. We create our plugin object with some basic information:
|
||||||
|
- `id`: A unique name for your plugin (use lowercase and dashes)
|
||||||
|
- `name`: A friendly name that users will see
|
||||||
|
- `description`: Explain what your plugin does
|
||||||
|
- `version`: Your plugin's version number
|
||||||
|
3. We set `disableToggle: true` so users can turn our plugin on/off in settings
|
||||||
|
4. The `run` function is where we put our plugin's code
|
||||||
|
5. We use `api.seqta.onMount` to wait for the homepage to load
|
||||||
|
6. We create and style a message element
|
||||||
|
7. We return a cleanup function that removes our changes when the plugin is disabled
|
||||||
|
|
||||||
|
## The Plugin API
|
||||||
|
|
||||||
|
When your plugin runs, it gets access to a powerful API that lets you do all sorts of things. Let's look at what you can do:
|
||||||
|
|
||||||
|
### SEQTA API (`api.seqta`)
|
||||||
|
|
||||||
|
This helps you interact with SEQTA's pages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Wait for an element to appear on the page
|
||||||
|
api.seqta.onMount('.some-class', (element) => {
|
||||||
|
// Do something with the element
|
||||||
|
});
|
||||||
|
|
||||||
|
// Know when the user changes pages
|
||||||
|
api.seqta.onPageChange((page) => {
|
||||||
|
console.log('User went to:', page);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the current page
|
||||||
|
const currentPage = api.seqta.getCurrentPage();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings API (`api.settings`)
|
||||||
|
|
||||||
|
Want to let users customize your plugin? Use settings!
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BasePlugin } from '@/plugins/core/settings';
|
||||||
|
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
|
||||||
|
|
||||||
|
// Define your settings
|
||||||
|
const settings = defineSettings({
|
||||||
|
showMessage: booleanSetting({
|
||||||
|
default: true,
|
||||||
|
title: "Show Welcome Message",
|
||||||
|
description: "Show a friendly message on the homepage",
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a class for your plugin
|
||||||
|
class MyPluginClass extends BasePlugin<typeof settings> {
|
||||||
|
@Setting(settings.showMessage)
|
||||||
|
showMessage!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create your plugin
|
||||||
|
const settingsInstance = new MyPluginClass();
|
||||||
|
|
||||||
|
const myPlugin: Plugin<typeof settings> = {
|
||||||
|
// ... other plugin details ...
|
||||||
|
settings: settingsInstance.settings,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Use the setting
|
||||||
|
if (api.settings.showMessage) {
|
||||||
|
// Show the message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for setting changes
|
||||||
|
api.settings.onChange('showMessage', (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
// Show the message
|
||||||
|
} else {
|
||||||
|
// Hide the message
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage API (`api.storage`)
|
||||||
|
|
||||||
|
Need to save some data? The storage API has got you covered:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Save some data
|
||||||
|
await api.storage.set('lastVisit', new Date().toISOString());
|
||||||
|
|
||||||
|
// Get it back later
|
||||||
|
const lastVisit = await api.storage.get('lastVisit');
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
api.storage.onChange('lastVisit', (newValue) => {
|
||||||
|
console.log('Last visit updated:', newValue);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Events API (`api.events`)
|
||||||
|
|
||||||
|
Want your plugin to be able to interface with other plugins? Then use events!
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Listen for an event
|
||||||
|
api.events.on('myCustomEvent', (data) => {
|
||||||
|
console.log('Got event:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send an event
|
||||||
|
api.events.emit('myCustomEvent', { some: 'data' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Styles
|
||||||
|
|
||||||
|
Want to make your plugin look pretty? You can add CSS styles:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const myPlugin: Plugin = {
|
||||||
|
// ... other plugin details ...
|
||||||
|
|
||||||
|
// Add your CSS here
|
||||||
|
styles: `
|
||||||
|
.my-plugin-message {
|
||||||
|
background: linear-gradient(135deg, #6e8efb, #a777e3);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 20px;
|
||||||
|
animation: slide-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in {
|
||||||
|
from { transform: translateY(-20px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Your plugin code here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
Here are some tips to make your plugin awesome:
|
||||||
|
|
||||||
|
1. **Always Clean Up**: When your plugin is disabled, clean up any changes you made:
|
||||||
|
```typescript
|
||||||
|
run: async (api) => {
|
||||||
|
// Add stuff to the page
|
||||||
|
const element = document.createElement('div');
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
// Return a cleanup function
|
||||||
|
return () => {
|
||||||
|
element.remove();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use TypeScript**: It helps catch errors before they happen and makes your code easier to understand.
|
||||||
|
|
||||||
|
3. **Test Your Plugin**: Make sure it works in different situations:
|
||||||
|
- When SEQTA is loading
|
||||||
|
- When the user switches pages
|
||||||
|
- When the plugin is enabled/disabled
|
||||||
|
- When settings are changed
|
||||||
|
|
||||||
|
4. **Keep It Fast**: Don't slow down SEQTA:
|
||||||
|
- Use `onMount` instead of intervals or timeouts
|
||||||
|
- Clean up event listeners when they're not needed
|
||||||
|
- Don't do heavy calculations on the main thread
|
||||||
|
|
||||||
|
5. **Make It User-Friendly**:
|
||||||
|
- Add clear settings with good descriptions
|
||||||
|
- Use `disableToggle: true` so users can turn it off if needed
|
||||||
|
- Add helpful error messages if something goes wrong
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Want to see more examples? Check out our built-in plugins:
|
||||||
|
- [themes](../../src/plugins/built-in/themes/index.ts): Shows how to change SEQTA's appearance
|
||||||
|
- [notificationCollector](../../src/plugins/built-in/notificationCollector/index.ts): Shows how to work with SEQTA's notifications
|
||||||
|
- [timetable](../../src/plugins/built-in/timetable/index.ts): Shows how to modify SEQTA's timetable view
|
||||||
|
- [assessmentsAverage](../../src/plugins/built-in/assessmentsAverage/index.ts): Shows how to add new features to existing pages
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
Got stuck? No worries! Here's where you can get help:
|
||||||
|
- Join our [Discord server](https://discord.gg/YzmbnCDkat)
|
||||||
|
- Check out the built-in plugins in the `src/plugins/built-in` folder
|
||||||
|
- Open an issue on our [GitHub page](https://github.com/betterseqta/betterseqta-plus/issues)
|
||||||
|
|
||||||
|
Happy coding and feel free to checkout the api reference [here](./api-reference.md)
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
# Plugin API Reference
|
||||||
|
|
||||||
|
This document provides detailed technical information about BetterSEQTA+'s plugin APIs. For a beginner-friendly introduction, see [Creating Your First Plugin](./README.md).
|
||||||
|
|
||||||
|
## Plugin Structure
|
||||||
|
|
||||||
|
Here's how a plugin is structured:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
import { BasePlugin } from '@/plugins/core/settings';
|
||||||
|
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
|
||||||
|
|
||||||
|
// First, define your settings
|
||||||
|
const settings = defineSettings({
|
||||||
|
enabled: booleanSetting({
|
||||||
|
default: true,
|
||||||
|
title: "Enable Feature",
|
||||||
|
description: "Turn this feature on or off",
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a class to handle your settings
|
||||||
|
class MyPluginClass extends BasePlugin<typeof settings> {
|
||||||
|
@Setting(settings.enabled)
|
||||||
|
enabled!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an instance of your settings
|
||||||
|
const settingsInstance = new MyPluginClass();
|
||||||
|
|
||||||
|
// Create your plugin
|
||||||
|
const myPlugin: Plugin<typeof settings> = {
|
||||||
|
id: 'my-plugin',
|
||||||
|
name: 'My Plugin',
|
||||||
|
description: 'A cool plugin that does things',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: settingsInstance.settings,
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
console.log('Plugin is running!');
|
||||||
|
|
||||||
|
// Do stuff when settings change
|
||||||
|
api.settings.onChange('enabled', (enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
console.log('Feature enabled!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a cleanup function
|
||||||
|
return () => {
|
||||||
|
console.log('Plugin cleanup');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default myPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEQTA API
|
||||||
|
|
||||||
|
The SEQTA API helps you interact with SEQTA's pages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
|
||||||
|
const seqtaPlugin: Plugin<typeof settings> = {
|
||||||
|
id: 'seqta-example',
|
||||||
|
name: 'SEQTA Example',
|
||||||
|
description: 'Shows how to use the SEQTA API',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Wait for elements to appear
|
||||||
|
const { unregister: timetableUnregister } = api.seqta.onMount('.timetable', (timetable) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.textContent = 'Export';
|
||||||
|
timetable.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track page changes
|
||||||
|
const { unregister: pageUnregister } = api.seqta.onPageChange((page) => {
|
||||||
|
console.log('User went to:', page);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when disabled
|
||||||
|
return () => {
|
||||||
|
timetableUnregister();
|
||||||
|
pageUnregister();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default seqtaPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Settings API
|
||||||
|
|
||||||
|
Here's how to add settings to your plugin:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
import { BasePlugin } from '@/plugins/core/settings';
|
||||||
|
import { booleanSetting, stringSetting, numberSetting, selectSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
|
||||||
|
|
||||||
|
// Define your settings
|
||||||
|
const settings = defineSettings({
|
||||||
|
darkMode: booleanSetting({
|
||||||
|
default: false,
|
||||||
|
title: "Dark Mode",
|
||||||
|
description: "Enable dark mode"
|
||||||
|
}),
|
||||||
|
userName: stringSetting({
|
||||||
|
default: "",
|
||||||
|
title: "User Name",
|
||||||
|
description: "Your display name",
|
||||||
|
placeholder: "Enter your name..."
|
||||||
|
}),
|
||||||
|
theme: selectSetting({
|
||||||
|
default: "light",
|
||||||
|
title: "Theme",
|
||||||
|
description: "Choose your theme",
|
||||||
|
options: [
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
{ value: "dark", label: "Dark" }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create your settings class
|
||||||
|
class ThemePluginClass extends BasePlugin<typeof settings> {
|
||||||
|
@Setting(settings.darkMode)
|
||||||
|
darkMode!: boolean;
|
||||||
|
|
||||||
|
@Setting(settings.userName)
|
||||||
|
userName!: string;
|
||||||
|
|
||||||
|
@Setting(settings.theme)
|
||||||
|
theme!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the plugin
|
||||||
|
const themePlugin: Plugin<typeof settings> = {
|
||||||
|
id: 'theme-example',
|
||||||
|
name: 'Theme Example',
|
||||||
|
description: 'Shows how to use settings',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: new ThemePluginClass().settings,
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Apply initial settings
|
||||||
|
if (api.settings.darkMode) {
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
const { unregister } = api.settings.onChange('darkMode', (enabled) => {
|
||||||
|
document.body.classList.toggle('dark', enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregister();
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default themePlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage API
|
||||||
|
|
||||||
|
Here's how to use storage in your plugin:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
|
||||||
|
const storagePlugin: Plugin<typeof settings> = {
|
||||||
|
id: 'storage-example',
|
||||||
|
name: 'Storage Example',
|
||||||
|
description: 'Shows how to use storage',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Wait for storage to be ready
|
||||||
|
await api.storage.loaded;
|
||||||
|
|
||||||
|
// Save some data
|
||||||
|
await api.storage.set('lastVisit', new Date().toISOString());
|
||||||
|
|
||||||
|
// Get saved data
|
||||||
|
const lastVisit = await api.storage.get('lastVisit');
|
||||||
|
console.log('Last visit:', lastVisit);
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
const { unregister } = api.storage.onChange('lastVisit', (newValue) => {
|
||||||
|
console.log('Last visit updated:', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregister();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default storagePlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events API
|
||||||
|
|
||||||
|
Here's how to use events in your plugin:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
|
||||||
|
const eventsPlugin: Plugin<typeof settings> = {
|
||||||
|
id: 'events-example',
|
||||||
|
name: 'Events Example',
|
||||||
|
description: 'Shows how to use events',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// Listen for theme changes
|
||||||
|
const { unregister: themeListener } = api.events.on('theme.changed', (theme) => {
|
||||||
|
console.log('Theme changed to:', theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for notifications
|
||||||
|
const { unregister: notifyListener } = api.events.on('notification.new', (notification) => {
|
||||||
|
console.log('New notification:', notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up listeners
|
||||||
|
return () => {
|
||||||
|
themeListener();
|
||||||
|
notifyListener();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default eventsPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
Here's how to write efficient plugins:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Plugin } from '@/plugins/core/types';
|
||||||
|
|
||||||
|
const efficientPlugin: Plugin<typeof settings> = {
|
||||||
|
id: 'efficient-example',
|
||||||
|
name: 'Efficient Example',
|
||||||
|
description: 'Shows performance best practices',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
// ✅ Good: Use onMount
|
||||||
|
const { unregister } = api.seqta.onMount('.timetable', (el) => {
|
||||||
|
el.classList.add('enhanced');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ Bad: Don't use intervals
|
||||||
|
// const interval = setInterval(() => {
|
||||||
|
// const el = document.querySelector('.timetable');
|
||||||
|
// if (el) el.classList.add('enhanced');
|
||||||
|
// }, 100);
|
||||||
|
|
||||||
|
// ✅ Good: Cache DOM elements
|
||||||
|
const header = document.querySelector('.header');
|
||||||
|
if (header) {
|
||||||
|
// Reuse header instead of querying again
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Good: Batch DOM updates
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
fragment.appendChild(div);
|
||||||
|
}
|
||||||
|
document.body.appendChild(fragment);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregister();
|
||||||
|
// clearInterval(interval); // If you used the bad approach
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default efficientPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
Each plugin should be in its own file and exported as the default export. The plugin should:
|
||||||
|
1. Import necessary types and helpers
|
||||||
|
2. Define settings if needed
|
||||||
|
3. Create a settings class if using settings
|
||||||
|
4. Create the plugin object with proper type annotation
|
||||||
|
5. Export the plugin as default
|
||||||
|
|
||||||
|
Remember to always:
|
||||||
|
- Use proper TypeScript types
|
||||||
|
- Clean up when your plugin is disabled
|
||||||
|
- Handle errors gracefully
|
||||||
|
- Follow the plugin structure shown above
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import mime from "mime-types";
|
||||||
|
|
||||||
|
export const base64Loader = {
|
||||||
|
name: "base64-loader",
|
||||||
|
transform(_: any, id: string) {
|
||||||
|
const [filePath, query] = id.split("?");
|
||||||
|
if (query !== "base64") return null;
|
||||||
|
|
||||||
|
const data = fs.readFileSync(filePath, { encoding: 'base64' });
|
||||||
|
const mimeType = mime.lookup(filePath);
|
||||||
|
const dataURL = `data:${mimeType};base64,${data}`;
|
||||||
|
|
||||||
|
return `export default '${dataURL}';`;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// ref: https://stackoverflow.com/a/76920975
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
|
export default function ClosePlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'ClosePlugin', // required, will show up in warnings and errors
|
||||||
|
|
||||||
|
// use this to catch errors when building
|
||||||
|
buildEnd(error) {
|
||||||
|
if(error) {
|
||||||
|
console.error('Error bundling')
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
} else {
|
||||||
|
console.log('Build ended')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// use this to catch the end of a build without errors
|
||||||
|
closeBundle() {
|
||||||
|
console.log('Bundle closed')
|
||||||
|
process.exit(0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Browser, BuildTarget, Manifest } from './types'
|
||||||
|
import type { AnyCase } from './utils'
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {Manifest} manifest
|
||||||
|
* @param {AnyCase<Browser>} browser
|
||||||
|
* @return {*} {@link BuildTarget}
|
||||||
|
*/
|
||||||
|
export function createManifest(
|
||||||
|
manifest: Manifest,
|
||||||
|
browser: AnyCase<Browser>,
|
||||||
|
): BuildTarget {
|
||||||
|
return {
|
||||||
|
manifest,
|
||||||
|
browser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a base Manifest to inherit from
|
||||||
|
* type Manifest = chrome.runtime.ManifestV3
|
||||||
|
*
|
||||||
|
* use as shared base to extend inBrowser manifests
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {Manifest} manifest
|
||||||
|
* @return {*} {@link Manifest}
|
||||||
|
*/
|
||||||
|
export function createManifestBase(manifest: Manifest): Manifest {
|
||||||
|
return manifest
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// vite-plugin-inline-worker-dev.ts
|
||||||
|
import { Plugin } from 'vite'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import { build, transform } from 'esbuild'
|
||||||
|
|
||||||
|
export default function InlineWorkerDevPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite:inline-worker-dev',
|
||||||
|
async load(id) {
|
||||||
|
if (id.includes('?inlineWorker')) {
|
||||||
|
const [cleanPath] = id.split('?')
|
||||||
|
console.log('cleanPath', cleanPath)
|
||||||
|
const code = await fs.readFile(cleanPath, 'utf-8')
|
||||||
|
const result = await build({
|
||||||
|
entryPoints: [cleanPath],
|
||||||
|
bundle: true,
|
||||||
|
write: false,
|
||||||
|
platform: 'browser',
|
||||||
|
format: 'iife',
|
||||||
|
target: 'esnext',
|
||||||
|
})
|
||||||
|
|
||||||
|
const workerCode = result.outputFiles[0].text
|
||||||
|
|
||||||
|
const workerBlobCode = `
|
||||||
|
const code = ${JSON.stringify(workerCode)};
|
||||||
|
export default function InlineWorker() {
|
||||||
|
const blob = new Blob([code], { type: 'application/javascript' });
|
||||||
|
return new Worker(URL.createObjectURL(blob), { type: 'module' });
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return workerBlobCode
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
TEMPORARY FIX FOR CHROME 130+ builds
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { PluginOption } from 'vite';
|
||||||
|
import { ManifestV3Export } from '@crxjs/vite-plugin';
|
||||||
|
|
||||||
|
const manifestPath = path.resolve('dist/chrome/manifest.json');
|
||||||
|
|
||||||
|
export function updateManifestPlugin(): PluginOption {
|
||||||
|
return {
|
||||||
|
name: 'update-manifest-plugin',
|
||||||
|
enforce: 'post',
|
||||||
|
closeBundle() {
|
||||||
|
forceDisableUseDynamicUrl();
|
||||||
|
},
|
||||||
|
|
||||||
|
configureServer(server) {
|
||||||
|
server.httpServer?.once('listening', () => {
|
||||||
|
const updated = forceDisableUseDynamicUrl();
|
||||||
|
if (updated) {
|
||||||
|
server.ws.send({ type: 'full-reload' });
|
||||||
|
console.log('** updated **');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement retry mechanism for file watching
|
||||||
|
const watchWithRetry = () => {
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
console.log('Manifest not found, retrying in 1 second...');
|
||||||
|
setTimeout(watchWithRetry, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.watchFile(manifestPath, () => {
|
||||||
|
try {
|
||||||
|
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
if (manifestContents.web_accessible_resources?.some((resource: any) => resource.use_dynamic_url)) {
|
||||||
|
const updated = forceDisableUseDynamicUrl();
|
||||||
|
if (updated) {
|
||||||
|
server.ws.send({ type: 'full-reload' });
|
||||||
|
console.log('** updated **');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error reading manifest, will retry on next change:', error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watchWithRetry();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
writeBundle() {
|
||||||
|
console.log('### writeBundle ##');
|
||||||
|
forceDisableUseDynamicUrl();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceDisableUseDynamicUrl() {
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Awaited<ManifestV3Export>;
|
||||||
|
|
||||||
|
if (typeof manifestContents === 'function' || !manifestContents.web_accessible_resources) return false;
|
||||||
|
if (manifestContents.web_accessible_resources.every((resource) => !resource.use_dynamic_url)) return false;
|
||||||
|
|
||||||
|
manifestContents.web_accessible_resources.forEach((resource) => {
|
||||||
|
if (resource.use_dynamic_url) resource.use_dynamic_url = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(manifestPath, JSON.stringify(manifestContents, null, 2));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
const glob = require('glob');
|
||||||
|
const semver = require('semver');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function getLatestVersion(files) {
|
||||||
|
console.log('Files passed to getLatestVersion:', files);
|
||||||
|
|
||||||
|
const versions = files.map(file => {
|
||||||
|
const match = file.match(/@([\d\.]+)-/);
|
||||||
|
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
|
||||||
|
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
|
||||||
|
const semverVersion = fullVersion.split('.').slice(0, 3).join('.'); // Trim to 3.4.5
|
||||||
|
|
||||||
|
return { fullVersion, semverVersion };
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
console.log('Extracted versions:', versions.map(v => v.semverVersion));
|
||||||
|
|
||||||
|
// Find latest version using the trimmed semver format
|
||||||
|
const latestSemver = semver.maxSatisfying(versions.map(v => v.semverVersion), '*');
|
||||||
|
console.log('Latest SemVer-compatible version:', latestSemver);
|
||||||
|
|
||||||
|
// Get the full version that matches the latest SemVer version
|
||||||
|
const latestVersion = versions.find(v => v.semverVersion === latestSemver)?.fullVersion || null;
|
||||||
|
|
||||||
|
console.log('Final selected latest version:', latestVersion);
|
||||||
|
return latestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestFiles(browser) {
|
||||||
|
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
||||||
|
console.log('Glob pattern:', pattern);
|
||||||
|
|
||||||
|
const files = glob.sync(pattern);
|
||||||
|
console.log('Files found for browser', browser, ':', files);
|
||||||
|
|
||||||
|
const latestVersion = getLatestVersion(files);
|
||||||
|
|
||||||
|
// Find the exact file by matching the original full version
|
||||||
|
const latestFile = files.find(file => file.includes(`@${latestVersion}-`));
|
||||||
|
|
||||||
|
console.log('Latest file for browser', browser, ':', latestFile);
|
||||||
|
return latestFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zipSources() {
|
||||||
|
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
|
||||||
|
|
||||||
|
const excludePatterns = [
|
||||||
|
'node_modules',
|
||||||
|
'dist',
|
||||||
|
'.env*',
|
||||||
|
'.git',
|
||||||
|
'.github',
|
||||||
|
'.vscode',
|
||||||
|
'LICENSE',
|
||||||
|
'package.json'
|
||||||
|
].map(pattern => `-x!${pattern}`).join(' ');
|
||||||
|
|
||||||
|
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
|
||||||
|
|
||||||
|
console.log('Zipping project sources with command:', zipCommand);
|
||||||
|
execSync(zipCommand, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
return zipFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPublishCommand(browsers) {
|
||||||
|
const chromeZip = browsers.includes('chrome') ? getLatestFiles('chrome') : null;
|
||||||
|
const firefoxZip = browsers.includes('firefox') ? getLatestFiles('firefox') : null;
|
||||||
|
const firefoxSourcesZip = browsers.includes('firefox') ? zipSources() : null;
|
||||||
|
|
||||||
|
console.log('Chrome zip:', chromeZip);
|
||||||
|
console.log('Firefox zip:', firefoxZip);
|
||||||
|
console.log('Firefox sources zip:', firefoxSourcesZip);
|
||||||
|
|
||||||
|
if (browsers.length === 0) {
|
||||||
|
console.log('No browsers specified. Exiting.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((browsers.includes('chrome') && !chromeZip) || (browsers.includes('firefox') && (!firefoxZip || !firefoxSourcesZip))) {
|
||||||
|
console.error('Could not find required zip files for specified browsers.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = 'publish-extension';
|
||||||
|
if (chromeZip) {
|
||||||
|
command += ` --chrome-zip ${chromeZip}`;
|
||||||
|
}
|
||||||
|
if (firefoxZip && firefoxSourcesZip) {
|
||||||
|
command += ` --firefox-zip ${firefoxZip} --firefox-sources-zip ${firefoxSourcesZip}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Running command:', command);
|
||||||
|
execSync(command, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command-line arguments
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const browserIndex = args.indexOf('--b');
|
||||||
|
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
|
||||||
|
|
||||||
|
runPublishCommand(browsers);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export default function touchGlobalCSSPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'touch-global-css',
|
||||||
|
handleHotUpdate({ modules }) {
|
||||||
|
// log all of the staticImportedUrls
|
||||||
|
const importers = modules[0]._clientModule.importers
|
||||||
|
importers.forEach((importer) => {
|
||||||
|
if (importer.file.includes('.css')) {
|
||||||
|
console.log("touching", importer.file)
|
||||||
|
fs.utimesSync(importer.file, new Date(), new Date())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
import type { ManifestV3Export } from '@crxjs/vite-plugin'
|
||||||
|
import { type AnyCase, createEnum } from './utils'
|
||||||
|
|
||||||
|
export const FrameworkEnum = {
|
||||||
|
React: 'React',
|
||||||
|
Vanilla: 'Vanilla',
|
||||||
|
Preact: 'Preact',
|
||||||
|
Lit: 'Lit',
|
||||||
|
Svelte: 'Svelte',
|
||||||
|
Vue: 'Vue',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const BrowserEnum = {
|
||||||
|
Chrome: 'Chrome',
|
||||||
|
Brave: 'Brave',
|
||||||
|
Opera: 'Opera',
|
||||||
|
Edge: 'Edge',
|
||||||
|
Firefox: 'Firefox',
|
||||||
|
Safari: 'Safari',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const LanguageEnum = {
|
||||||
|
TypeScript: 'TypeScript',
|
||||||
|
JavaScript: 'JavaScript',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const StyleEnum = {
|
||||||
|
Tailwind: 'Tailwind',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const PackageManagerEnum = {
|
||||||
|
Bun: 'Bun',
|
||||||
|
PnPm: 'PnPm',
|
||||||
|
Npm: 'Npm',
|
||||||
|
Yarn: 'Yarn',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
|
||||||
|
export type BrowserSpecificSettings = {
|
||||||
|
browser_specific_settings?: {
|
||||||
|
gecko?: {
|
||||||
|
id: string
|
||||||
|
strict_min_version?: string
|
||||||
|
strict_max_version?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Manifest = ManifestV3Export
|
||||||
|
export type ManifestIcons = chrome.runtime.ManifestIcons
|
||||||
|
export type ManifestBackground = chrome.runtime.ManifestV3['background']
|
||||||
|
export type ManifestContentScripts =
|
||||||
|
chrome.runtime.ManifestV3['content_scripts']
|
||||||
|
export type ManifestWebAccessibleResources =
|
||||||
|
chrome.runtime.ManifestV3['web_accessible_resources']
|
||||||
|
export type ManifestCommands = chrome.runtime.ManifestV3['commands']
|
||||||
|
export type ManifestAction = chrome.runtime.ManifestV3['action']
|
||||||
|
export type ManifestPermissions = chrome.runtime.ManifestV3['permissions']
|
||||||
|
export type ManifestOptionsUI = chrome.runtime.ManifestV3['options_ui']
|
||||||
|
export type ManifestURLOverrides =
|
||||||
|
chrome.runtime.ManifestV3['chrome_url_overrides']
|
||||||
|
|
||||||
|
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>
|
||||||
|
export type BrowserEnumType<T extends string> = {
|
||||||
|
[browser in BrowserName<T>]: BrowserName<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BuildMode = AnyCase<Browser>
|
||||||
|
export type BuildTarget = {
|
||||||
|
manifest: Manifest
|
||||||
|
browser: AnyCase<Browser>
|
||||||
|
}
|
||||||
|
export type BuildConfig = {
|
||||||
|
command?: 'build' | 'serve'
|
||||||
|
mode?: AnyCase<Browser> | string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Repository {
|
||||||
|
type: string
|
||||||
|
url?: string
|
||||||
|
bugs?: Bugs
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Bugs {
|
||||||
|
url?: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum]
|
||||||
|
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum)
|
||||||
|
|
||||||
|
export type PackageManager =
|
||||||
|
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum]
|
||||||
|
export const PackageManager: AnyCase<PackageManager> =
|
||||||
|
createEnum(PackageManagerEnum)
|
||||||
|
|
||||||
|
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum]
|
||||||
|
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum)
|
||||||
|
|
||||||
|
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum]
|
||||||
|
export const Style: AnyCase<Style> = createEnum(StyleEnum)
|
||||||
|
|
||||||
|
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum]
|
||||||
|
export const Language: AnyCase<Language> = createEnum(LanguageEnum)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export type ObjectValues<T> = T[keyof T]
|
||||||
|
|
||||||
|
export function createEnum<T extends Record<string, string>>(enumObj: T) {
|
||||||
|
return Object.values(enumObj) as unknown as ObjectValues<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyCase<T extends string> =
|
||||||
|
| Uppercase<T>
|
||||||
|
| Lowercase<T>
|
||||||
|
| Capitalize<T>
|
||||||
|
| Uncapitalize<T>
|
||||||
|
|
||||||
|
export type AnyCaseLanguage<T extends string, K extends string> =
|
||||||
|
| Uppercase<T | K>
|
||||||
|
| Lowercase<T | K>
|
||||||
|
| Capitalize<T | K>
|
||||||
|
| Uncapitalize<T | K>
|
||||||
|
|
||||||
|
export type OptionalKeys<T> = {
|
||||||
|
[K in keyof T as undefined extends T[K] ? K : never]: T[K]
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "BetterSEQTA+",
|
|
||||||
"version": "3.2.6",
|
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development, and incorporate a plethora of new features!",
|
|
||||||
"icons": {
|
|
||||||
"32": "src/resources/icons/icon-32.png",
|
|
||||||
"48": "src/resources/icons/icon-48.png",
|
|
||||||
"64": "src/resources/icons/icon-64.png"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"browser_style": true,
|
|
||||||
"default_popup": "src/interface/index.html#settings",
|
|
||||||
"default_icon": {
|
|
||||||
"32": "src/resources/icons/icon-32.png",
|
|
||||||
"48": "src/resources/icons/icon-48.png",
|
|
||||||
"64": "src/resources/icons/icon-64.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": ["tabs", "notifications", "storage", "activeTab"],
|
|
||||||
"host_permissions": ["<all_urls>"],
|
|
||||||
"background": {
|
|
||||||
"scripts": ["src/background.ts"]
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"matches": ["*://*/*"],
|
|
||||||
"js": ["src/SEQTA.ts"],
|
|
||||||
"run_at": "document_start"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "BetterSEQTA+",
|
|
||||||
"version": "3.3.1",
|
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
|
||||||
"icons": {
|
|
||||||
"32": "src/resources/icons/icon-32.png",
|
|
||||||
"48": "src/resources/icons/icon-48.png",
|
|
||||||
"64": "src/resources/icons/icon-64.png"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"browser_style": true,
|
|
||||||
"default_popup": "src/interface/index.html#settings",
|
|
||||||
"default_icon": {
|
|
||||||
"32": "src/resources/icons/icon-32.png",
|
|
||||||
"48": "src/resources/icons/icon-48.png",
|
|
||||||
"64": "src/resources/icons/icon-64.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": ["tabs", "notifications", "storage"],
|
|
||||||
"host_permissions": ["https://newsapi.org/", "*://*/*"],
|
|
||||||
"background": {
|
|
||||||
"service_worker": "src/background.ts"
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"matches": ["*://*/*"],
|
|
||||||
"js": ["src/SEQTA.ts"],
|
|
||||||
"run_at": "document_start"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"web_accessible_resources": [
|
|
||||||
{
|
|
||||||
"resources": ["src/interface/index.html"],
|
|
||||||
"matches": ["*://*/*"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"resources": ["src/seqta/ui/background/background.html"],
|
|
||||||
"matches": ["*://*/*"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"resources": ["*://*/*"],
|
|
||||||
"matches": ["*://*/*"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+80
-57
@@ -1,15 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "betterseqtaplus",
|
"name": "betterseqtaplus",
|
||||||
"version": "3.3.0",
|
"version": "3.4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development, and incorporate a plethora of new features!",
|
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "cross-env MODE=chrome vite dev",
|
||||||
"dev:firefox": "VITE_TARGET=firefox vite build --watch",
|
"dev:firefox": "cross-env MODE=firefox vite build --watch",
|
||||||
"build": "vite build",
|
"build": "cross-env MODE=chrome vite build && cross-env MODE=firefox vite build",
|
||||||
"build:firefox": "VITE_TARGET=firefox vite build",
|
"build:chrome": "cross-env MODE=chrome vite build",
|
||||||
"package": "rimraf ./dist/*.map && 7z a -tzip extension.zip ./dist/*"
|
"build:firefox": "cross-env MODE=firefox vite build",
|
||||||
|
"build:safari": "cross-env MODE=safari vite build",
|
||||||
|
"build:dev": "cross-env MODE=chrome SOURCEMAP=true vite build && cross-env MODE=firefox SOURCEMAP=true vite build",
|
||||||
|
"convert:safari": "xcrun safari-web-extension-converter dist/safari --project-location . --app-name $npm_package_name-safari",
|
||||||
|
"dependency-graph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg",
|
||||||
|
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
|
||||||
|
"publish": "bun lib/publish.js --b",
|
||||||
|
"zip": "bedframe zip"
|
||||||
},
|
},
|
||||||
"targets": {
|
"targets": {
|
||||||
"prod": {
|
"prod": {
|
||||||
@@ -19,64 +26,80 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": {
|
||||||
|
"name": "SethBurkart123",
|
||||||
|
"email": "betterseqta@betterseqta.com",
|
||||||
|
"url": "https://github.com/BetterSEQTA/BetterSEQTA-plus"
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@crxjs/vite-plugin": "^2.0.0-beta.23",
|
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||||
|
"@babel/runtime": "^7.26.9",
|
||||||
|
"@bedframe/cli": "^0.0.91",
|
||||||
|
"@crxjs/vite-plugin": "2.0.0-beta.25",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
"@types/react": "^19.0.10",
|
||||||
"eslint": "^8.56.0",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dependency-cruiser": "^16.10.0",
|
||||||
|
"eslint": "9.22.0",
|
||||||
|
"glob": "^11.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"parcel": "^2.11.0",
|
"prettier": "^3.5.3",
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"sass": "^1.70.0",
|
"publish-browser-extension": "^3.0.0",
|
||||||
"sass-loader": "^13.3.3",
|
"sass": "^1.85.1",
|
||||||
"url": "^0.11.3"
|
"sass-loader": "^16.0.5",
|
||||||
|
"semver": "^7.7.1",
|
||||||
|
"tailwindcss": "3",
|
||||||
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocknote/core": "^0.14.1",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@blocknote/mantine": "^0.14.1",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@blocknote/react": "^0.14.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-less": "^6.0.2",
|
"@codemirror/language": "^6.10.8",
|
||||||
"@heroicons/react": "^2.1.3",
|
"@codemirror/search": "^6.5.10",
|
||||||
"@million/lint": "latest",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@codemirror/view": "^6.36.4",
|
||||||
"@types/color": "^3.0.6",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@types/lodash": "^4.17.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/node": "^20.11.30",
|
"@types/chrome": "^0.0.308",
|
||||||
"@types/react": "^18.2.55",
|
"@types/color": "^4.2.0",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/sortablejs": "^1.15.7",
|
"@types/node": "^22.13.10",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/webextension-polyfill": "^0.10.7",
|
"@types/uuid": "^10.0.0",
|
||||||
"@uiw/codemirror-extensions-color": "^4.21.25",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
"@uiw/codemirror-theme-github": "^4.21.25",
|
"@uiw/codemirror-extensions-color": "^4.23.10",
|
||||||
"@uiw/react-codemirror": "^4.21.25",
|
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.21",
|
||||||
"classnames": "^2.5.1",
|
"client-vector-search": "../client-vector-search",
|
||||||
"color": "^4.2.3",
|
"codemirror": "^6.0.1",
|
||||||
"dompurify": "^3.0.8",
|
"color": "^5.0.0",
|
||||||
"framer-motion": "^11.0.25",
|
"dompurify": "^3.2.4",
|
||||||
|
"embla-carousel-autoplay": "^8.5.2",
|
||||||
|
"embla-carousel-svelte": "^8.5.2",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"flexsearch": "^0.8.147",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
|
"idb": "^8.0.2",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"million": "latest",
|
"mathjs": "^14.4.0",
|
||||||
"motion": "^10.17.0",
|
"million": "^3.1.11",
|
||||||
"react": "^18.2.0",
|
"motion": "^12.4.12",
|
||||||
"react-best-gradient-color-picker": "3.0.5",
|
"postcss": "^8.5.3",
|
||||||
"react-dom": "^18.2.0",
|
"react": "17",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-best-gradient-color-picker": "3.0.11",
|
||||||
"react-router-dom": "^6.22.0",
|
"react-dom": "17",
|
||||||
"react-toastify": "^10.0.5",
|
"rss-parser": "^3.13.0",
|
||||||
"rimraf": "^5.0.5",
|
"sortablejs": "^1.15.6",
|
||||||
"sortablejs": "^1.15.2",
|
"svelte": "^5.22.6",
|
||||||
"swiper": "latest",
|
"typescript": "^5.8.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"uuid": "^11.1.0",
|
||||||
"ts-loader": "^9.5.1",
|
"vite": "^6.2.1",
|
||||||
"typescript": "^5.3.3",
|
"webextension-polyfill": "^0.12.0"
|
||||||
"uuid": "^9.0.1",
|
|
||||||
"vite": "^5.2.2",
|
|
||||||
"webextension-polyfill": "^0.10.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
--- a/Users/sethburkart/Documents/Coding/betterseqta-plus/src/plugins/core/settings.ts
|
||||||
|
+++ b/Users/sethburkart/Documents/Coding/betterseqta-plus/src/plugins/core/settings.ts
|
||||||
|
@@ -2,7 +2,7 @@
|
||||||
|
|
||||||
|
// Base interfaces for our settings
|
||||||
|
interface BaseSettingOptions {
|
||||||
|
- title: string;
|
||||||
|
+ readonly title: string; // Mark as readonly where appropriate
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -11,21 +11,21 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StringSettingOptions extends BaseSettingOptions {
|
||||||
|
- default: string;
|
||||||
|
+ readonly default: string;
|
||||||
|
maxLength?: number;
|
||||||
|
pattern?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberSettingOptions extends BaseSettingOptions {
|
||||||
|
- default: number;
|
||||||
|
+ readonly default: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectSettingOptions<T extends string> extends BaseSettingOptions {
|
||||||
|
- default: T;
|
||||||
|
- options: readonly T[];
|
||||||
|
+ readonly default: T;
|
||||||
|
+ readonly options: readonly T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual decorators
|
||||||
|
@@ -34,14 +34,16 @@
|
||||||
|
// Ensure the settings property exists on the constructor's prototype
|
||||||
|
const proto = target.constructor.prototype;
|
||||||
|
if (!proto.hasOwnProperty('settings')) {
|
||||||
|
- proto.settings = {};
|
||||||
|
+ // Initialize with a base type that can be extended
|
||||||
|
+ Object.defineProperty(proto, 'settings', {
|
||||||
|
+ value: {},
|
||||||
|
+ writable: true, // Allows adding properties
|
||||||
|
+ configurable: true,
|
||||||
|
+ enumerable: true
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
// Add the setting to the prototype's settings object with const assertion
|
||||||
|
proto.settings[propertyKey] = {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
- };
|
||||||
|
-}
|
||||||
|
-
|
||||||
|
-export function StringSetting(options: StringSettingOptions): PropertyDecorator {
|
||||||
|
- return (target: Object, propertyKey: string | symbol) => {
|
||||||
|
- // Ensure the settings property exists on the constructor's prototype
|
||||||
|
- const proto = target.constructor.prototype;
|
||||||
|
- if (!proto.hasOwnProperty('settings')) {
|
||||||
|
- proto.settings = {};
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- // Add the setting to the prototype's settings object with const assertion
|
||||||
|
- proto.settings[propertyKey] = {
|
||||||
|
- type: 'string' as const,
|
||||||
|
- ...options
|
||||||
|
- };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -50,14 +52,16 @@
|
||||||
|
// Ensure the settings property exists on the constructor's prototype
|
||||||
|
const proto = target.constructor.prototype;
|
||||||
|
if (!proto.hasOwnProperty('settings')) {
|
||||||
|
- proto.settings = {};
|
||||||
|
+ Object.defineProperty(proto, 'settings', {
|
||||||
|
+ value: {},
|
||||||
|
+ writable: true,
|
||||||
|
+ configurable: true,
|
||||||
|
+ enumerable: true
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
-
|
||||||
|
+
|
||||||
|
// Add the setting to the prototype's settings object with const assertion
|
||||||
|
proto.settings[propertyKey] = {
|
||||||
|
type: 'number' as const,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
- };
|
||||||
|
-}
|
||||||
|
-
|
||||||
|
-export function SelectSetting<T extends string>(options: SelectSettingOptions<T>): PropertyDecorator {
|
||||||
|
- return (target: Object, propertyKey: string | symbol) => {
|
||||||
|
- // Ensure the settings property exists on the constructor's prototype
|
||||||
|
- const proto = target.constructor.prototype;
|
||||||
|
- if (!proto.hasOwnProperty('settings')) {
|
||||||
|
- proto.settings = {};
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- // Add the setting to the prototype's settings object with const assertion
|
||||||
|
- proto.settings[propertyKey] = {
|
||||||
|
- type: 'select' as const,
|
||||||
|
- ...options
|
||||||
|
- };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base plugin class that handles settings
|
||||||
|
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||||
|
// The settings property will be populated by decorators
|
||||||
|
- settings!: T;
|
||||||
|
-
|
||||||
|
+ // Keep the instance property and constructor logic as is,
|
||||||
|
+ // as changing it would require changing animated-background/index.ts
|
||||||
|
+ settings!: T; // Use definite assignment assertion
|
||||||
|
+
|
||||||
|
constructor() {
|
||||||
|
// Copy settings from the prototype to the instance
|
||||||
|
// This ensures that each instance has its own settings object
|
||||||
+41
-2481
File diff suppressed because it is too large
Load Diff
+90
-154
@@ -1,61 +1,6 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
import { SettingsState } from "./types/storage";
|
import type { SettingsState } from "@/types/storage";
|
||||||
|
import { fetchNews } from './background/news';
|
||||||
export const openDB = () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('MyDatabase', 1);
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event: any) => {
|
|
||||||
const db = event.target.result;
|
|
||||||
db.createObjectStore('backgrounds', { keyPath: 'id' });
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
|
||||||
resolve(request.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = (event: any) => {
|
|
||||||
reject('Error opening database: ' + event.target.errorCode);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const writeData = async (type: any, data: any) => {
|
|
||||||
const db: any = await openDB();
|
|
||||||
|
|
||||||
const tx = db.transaction('backgrounds', 'readwrite');
|
|
||||||
const store = tx.objectStore('backgrounds');
|
|
||||||
const request = await store.put({ id: 'customBackground', type, data });
|
|
||||||
|
|
||||||
return request.result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readData = () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
openDB()
|
|
||||||
.then((db: any) => {
|
|
||||||
const tx = db.transaction('backgrounds', 'readonly');
|
|
||||||
const store = tx.objectStore('backgrounds');
|
|
||||||
|
|
||||||
// Retrieve the custom background
|
|
||||||
const getRequest = store.get('customBackground');
|
|
||||||
|
|
||||||
// Attach success and error event handlers
|
|
||||||
getRequest.onsuccess = function(event: any) {
|
|
||||||
resolve(event.target.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
getRequest.onerror = function(event: any) {
|
|
||||||
console.error('An error occurred:', event);
|
|
||||||
reject(event);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('An error occurred:', error);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function reloadSeqtaPages() {
|
function reloadSeqtaPages() {
|
||||||
const result = browser.tabs.query({})
|
const result = browser.tabs.query({})
|
||||||
@@ -69,70 +14,50 @@ function reloadSeqtaPages() {
|
|||||||
result.then(open, console.error)
|
result.then(open, console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main message listener
|
// @ts-ignore
|
||||||
browser.runtime.onMessage.addListener((request: any, _sender: any, sendResponse: any) => {
|
browser.runtime.onMessage.addListener((request: any, _: any, sendResponse: (response?: any) => void) => {
|
||||||
|
|
||||||
switch (request.type) {
|
switch (request.type) {
|
||||||
case 'reloadTabs':
|
case 'reloadTabs':
|
||||||
reloadSeqtaPages();
|
reloadSeqtaPages();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'extensionPages':
|
|
||||||
browser.tabs.query({}).then(function (tabs) {
|
|
||||||
for (let tab of tabs) {
|
|
||||||
if (tab.url?.includes('chrome-extension://')) {
|
|
||||||
browser.tabs.sendMessage(tab.id!, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'currentTab':
|
|
||||||
browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
|
|
||||||
browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) {
|
|
||||||
sendResponse(response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 'githubTab':
|
|
||||||
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' });
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'setDefaultStorage':
|
case 'extensionPages':
|
||||||
SetStorageValue(DefaultValues);
|
browser.tabs.query({}).then(function (tabs) {
|
||||||
break;
|
for (let tab of tabs) {
|
||||||
|
if (tab.url?.includes('chrome-extension://')) {
|
||||||
|
browser.tabs.sendMessage(tab.id!, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'currentTab':
|
||||||
|
browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
|
||||||
|
browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) {
|
||||||
|
sendResponse(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
|
||||||
case 'sendNews':
|
case 'githubTab':
|
||||||
const date = new Date();
|
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' });
|
||||||
|
break;
|
||||||
const from =
|
|
||||||
date.getFullYear() +
|
|
||||||
'-' +
|
|
||||||
(date.getMonth() + 1) +
|
|
||||||
'-' +
|
|
||||||
(date.getDate() - 5);
|
|
||||||
|
|
||||||
const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`;
|
|
||||||
|
|
||||||
GetNews(sendResponse, url);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
case 'setDefaultStorage':
|
||||||
console.log('Unknown request type');
|
SetStorageValue(DefaultValues);
|
||||||
}
|
break;
|
||||||
});
|
|
||||||
|
|
||||||
function GetNews(sendResponse: any, url: string) {
|
case 'sendNews':
|
||||||
fetch(url)
|
fetchNews(request.source ?? 'australia', sendResponse);
|
||||||
.then((result) => result.json())
|
return true;
|
||||||
.then((response) => {
|
|
||||||
if (response.code == 'rateLimited') {
|
default:
|
||||||
GetNews(sendResponse, url += '%00');
|
console.log('Unknown request type');
|
||||||
} else {
|
}
|
||||||
sendResponse({ news: response });
|
|
||||||
}
|
return false;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultValues: SettingsState = {
|
const DefaultValues: SettingsState = {
|
||||||
onoff: true,
|
onoff: true,
|
||||||
@@ -140,7 +65,6 @@ const DefaultValues: SettingsState = {
|
|||||||
bksliderinput: "50",
|
bksliderinput: "50",
|
||||||
transparencyEffects: false,
|
transparencyEffects: false,
|
||||||
lessonalert: true,
|
lessonalert: true,
|
||||||
notificationcollector: true,
|
|
||||||
defaultmenuorder: [],
|
defaultmenuorder: [],
|
||||||
menuitems: {
|
menuitems: {
|
||||||
assessments: { toggle: true },
|
assessments: { toggle: true },
|
||||||
@@ -167,6 +91,7 @@ const DefaultValues: SettingsState = {
|
|||||||
originalSelectedColor: '',
|
originalSelectedColor: '',
|
||||||
DarkMode: true,
|
DarkMode: true,
|
||||||
animations: true,
|
animations: true,
|
||||||
|
assessmentsAverage: true,
|
||||||
defaultPage: 'home',
|
defaultPage: 'home',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
@@ -219,6 +144,8 @@ const DefaultValues: SettingsState = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
customshortcuts: [],
|
customshortcuts: [],
|
||||||
|
lettergrade: false,
|
||||||
|
newsSource: 'australia',
|
||||||
};
|
};
|
||||||
|
|
||||||
function SetStorageValue(object: any) {
|
function SetStorageValue(object: any) {
|
||||||
@@ -227,54 +154,63 @@ function SetStorageValue(object: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function UpdateCurrentValues() {
|
function convertBksliderToSpeed(bksliderinput: number): number {
|
||||||
try {
|
const minBase = 50;
|
||||||
const items = await browser.storage.local.get();
|
const maxBase = 150;
|
||||||
const CurrentValues = items;
|
|
||||||
|
|
||||||
const NewValue = Object.assign({}, DefaultValues, CurrentValues);
|
const scaledValue = 2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
|
||||||
|
const baseSpeed = 3;
|
||||||
|
|
||||||
function CheckInnerElement(element: any) {
|
const speed = baseSpeed / scaledValue;
|
||||||
for (let i in element) {
|
return speed;
|
||||||
if (typeof element[i] === 'object') {
|
}
|
||||||
// @ts-expect-error
|
|
||||||
if (!Array.isArray(DefaultValues[i])) {
|
|
||||||
// @ts-expect-error
|
|
||||||
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
|
|
||||||
} else {
|
|
||||||
// @ts-expect-error
|
|
||||||
const length = DefaultValues[i].length;
|
|
||||||
// @ts-expect-error
|
|
||||||
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
|
|
||||||
let NewArray = [];
|
|
||||||
for (let j = 0; j < length; j++) {
|
|
||||||
NewArray.push(NewValue[i][j]);
|
|
||||||
}
|
|
||||||
NewValue[i] = NewArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckInnerElement(DefaultValues);
|
async function migrateLegacySettings() {
|
||||||
|
const storage = await browser.storage.local.get(null) as unknown as SettingsState;
|
||||||
|
|
||||||
if (items['customshortcuts']) {
|
// Animated Background Migration
|
||||||
NewValue['customshortcuts'] = items['customshortcuts'];
|
if ('animatedbk' in storage || 'bksliderinput' in storage) {
|
||||||
}
|
const animatedSettings = {
|
||||||
|
enabled: storage.animatedbk ?? true,
|
||||||
SetStorageValue(NewValue);
|
speed: storage.bksliderinput ? convertBksliderToSpeed(parseFloat(storage.bksliderinput)) : 1
|
||||||
console.log('[BetterSEQTA+] Values updated successfully');
|
};
|
||||||
} catch (error) {
|
await browser.storage.local.set({ 'plugin.animated-background.settings': animatedSettings });
|
||||||
console.error('[BetterSEQTA+] Error updating values:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assessments Average Migration
|
||||||
|
if ('assessmentsAverage' in storage || 'lettergrade' in storage) {
|
||||||
|
const assessmentsSettings = {
|
||||||
|
enabled: storage.assessmentsAverage ?? true,
|
||||||
|
lettergrade: storage.lettergrade ?? false
|
||||||
|
};
|
||||||
|
await browser.storage.local.set({ 'plugin.assessments-average.settings': assessmentsSettings });
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('selectedTheme' in storage) {
|
||||||
|
const themesSettings = { enabled: true };
|
||||||
|
await browser.storage.local.set({ 'plugin.themes.settings': themesSettings });
|
||||||
|
}
|
||||||
|
if (storage.notificationCollector !== false) {
|
||||||
|
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: true } });
|
||||||
|
} else {
|
||||||
|
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysToRemove = [
|
||||||
|
'animatedbk',
|
||||||
|
'bksliderinput',
|
||||||
|
'assessmentsAverage',
|
||||||
|
'lettergrade'
|
||||||
|
];
|
||||||
|
await browser.storage.local.remove(keysToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onInstalled.addListener(function (event) {
|
browser.runtime.onInstalled.addListener(function (event) {
|
||||||
browser.storage.local.remove(['justupdated']);
|
browser.storage.local.remove(['justupdated']);
|
||||||
browser.storage.local.remove(['data']);
|
browser.storage.local.remove(['data']);
|
||||||
|
|
||||||
UpdateCurrentValues();
|
if ( event.reason == 'install' || event.reason == 'update' ) {
|
||||||
if ( event.reason == 'install', event.reason == 'update' ) {
|
|
||||||
browser.storage.local.set({ justupdated: true });
|
browser.storage.local.set({ justupdated: true });
|
||||||
|
migrateLegacySettings();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import Parser from 'rss-parser';
|
||||||
|
|
||||||
|
const fetchAustraliaNews = async (url: string, sendResponse: any) => {
|
||||||
|
fetch(url)
|
||||||
|
.then((result) => result.json())
|
||||||
|
.then((response) => {
|
||||||
|
if (response.code == 'rateLimited') {
|
||||||
|
fetchAustraliaNews(url += '%00', sendResponse);
|
||||||
|
} else {
|
||||||
|
sendResponse({ news: response });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rssFeedsByCountry: Record<string, string[]> = {
|
||||||
|
usa: [
|
||||||
|
"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
||||||
|
"https://www.huffpost.com/section/front-page/feed",
|
||||||
|
"https://www.npr.org/rss/rss.php",
|
||||||
|
],
|
||||||
|
taiwan: [
|
||||||
|
"https://news.ltn.com.tw/rss/all.xml",
|
||||||
|
"https://www.taipeitimes.com/xml/index.rss",
|
||||||
|
"https://international.thenewslens.com/rss",
|
||||||
|
],
|
||||||
|
hong_kong: [
|
||||||
|
"https://rthk9.rthk.hk/rthk/news/rss/e_expressnews_elocal.xml",
|
||||||
|
"https://www.scmp.com/rss/91/feed",
|
||||||
|
],
|
||||||
|
panama: [
|
||||||
|
"https://critica.com.pa/rss.xml",
|
||||||
|
"https://www.panamaamerica.com.pa/rss.xml",
|
||||||
|
"https://noticiassin.com/feed/",
|
||||||
|
"https://elcapitalfinanciero.com/feed/"
|
||||||
|
],
|
||||||
|
canada: [
|
||||||
|
"https://www.cbc.ca/cmlink/rss-topstories",
|
||||||
|
"https://calgaryherald.com/feed",
|
||||||
|
"https://ottawacitizen.com/feed",
|
||||||
|
"https://www.montrealgazette.com/feed"
|
||||||
|
],
|
||||||
|
singapore: [
|
||||||
|
"https://www.straitstimes.com/news/singapore/rss.xml",
|
||||||
|
"https://www.channelnewsasia.com/rssfeeds/8395986",
|
||||||
|
],
|
||||||
|
uk: [
|
||||||
|
"http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
"https://www.theguardian.com/uk/rss",
|
||||||
|
],
|
||||||
|
japan: [
|
||||||
|
"https://www3.nhk.or.jp/nhkworld/en/news/feeds/",
|
||||||
|
"https://news.livedoor.com/topics/rss/int.xml"
|
||||||
|
],
|
||||||
|
netherlands: [
|
||||||
|
"https://www.dutchnews.nl/feed/",
|
||||||
|
"https://www.nrc.nl/rss/"
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchNews(source: string, sendResponse: any) {
|
||||||
|
if (source === "australia") {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const from =
|
||||||
|
date.getFullYear() +
|
||||||
|
'-' +
|
||||||
|
(date.getMonth() + 1) +
|
||||||
|
'-' +
|
||||||
|
(date.getDate() - 5);
|
||||||
|
|
||||||
|
const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`;
|
||||||
|
fetchAustraliaNews(url, sendResponse);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new Parser();
|
||||||
|
let feeds: string[];
|
||||||
|
console.log('fetchNews', source)
|
||||||
|
|
||||||
|
if (rssFeedsByCountry[source.toLowerCase()]) {
|
||||||
|
// If the source is a country, fetch from predefined feeds
|
||||||
|
feeds = rssFeedsByCountry[source.toLowerCase()];
|
||||||
|
} else if (source.startsWith("http")) {
|
||||||
|
// If the source is a URL, use it directly
|
||||||
|
feeds = [source];
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid source. Provide a country code or a valid RSS feed URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const articlesPromises = feeds.map(async (feedUrl) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(feedUrl);
|
||||||
|
const feedString = await response.text();
|
||||||
|
const feed = await parser.parseString(feedString);
|
||||||
|
|
||||||
|
return feed.items.map((item) => ({
|
||||||
|
title: item.title || "",
|
||||||
|
description: item.contentSnippet || "",
|
||||||
|
url: item.link || "",
|
||||||
|
urlToImage: null,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch RSS feed: ${feedUrl}`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const articlesArray = await Promise.all(articlesPromises);
|
||||||
|
const articles = articlesArray.flat();
|
||||||
|
|
||||||
|
sendResponse({ news: { articles } });
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
|
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import './injected/popup.scss';
|
@use 'injected/popup.scss';
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background: #161616 !important;
|
background: #161616 !important;
|
||||||
|
|||||||
+613
-241
File diff suppressed because it is too large
Load Diff
@@ -36,4 +36,5 @@
|
|||||||
transform-origin: 70% 0;
|
transform-origin: 70% 0;
|
||||||
will-change: opacity, transform;
|
will-change: opacity, transform;
|
||||||
transform: translateZ(0); // promotes GPU rendering
|
transform: translateZ(0); // promotes GPU rendering
|
||||||
|
transition: opacity 0.05s, transform 0.05s;
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ html.transparencyEffects:not(.dark) {
|
|||||||
|
|
||||||
html.transparencyEffects {
|
html.transparencyEffects {
|
||||||
/* Background Fixes */
|
/* Background Fixes */
|
||||||
.notifications__item___2ErJN,
|
[class*="notifications__item___"],
|
||||||
#shortcuts {
|
#shortcuts {
|
||||||
backdrop-filter: unset !important;
|
backdrop-filter: unset !important;
|
||||||
}
|
}
|
||||||
@@ -24,21 +24,21 @@ html.transparencyEffects {
|
|||||||
/* Blurs */
|
/* Blurs */
|
||||||
.draggable,
|
.draggable,
|
||||||
.notice,
|
.notice,
|
||||||
.BasicPanel__BasicPanel___1GP6s,
|
[class*="BasicPanel__BasicPanel___"],
|
||||||
.message.addMessage,
|
.message.addMessage,
|
||||||
.singleSelect,
|
.singleSelect,
|
||||||
.uiFileHandlerPanel,
|
.uiFileHandlerPanel,
|
||||||
.Module__wrapper___2sbOo,
|
[class*="Module__wrapper___"],
|
||||||
.notifications__list___rp2L2,
|
[class*="notifications__list___"],
|
||||||
.thread,
|
.thread,
|
||||||
.calendar,
|
.calendar,
|
||||||
.navigator,
|
.navigator,
|
||||||
#title,
|
#title,
|
||||||
.LabelList__selected___3Egk7,
|
[class*="LabelList__selected___"],
|
||||||
.buttonChecklist,
|
.buttonChecklist,
|
||||||
.pane,
|
.pane,
|
||||||
.legacy-root button, .legacy-root a,
|
.legacy-root button, .legacy-root a,
|
||||||
.MessageList__MessageList___3DxoC {
|
[class*="MessageList__MessageList___"] {
|
||||||
backdrop-filter: blur(80px);
|
backdrop-filter: blur(80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ html.transparencyEffects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.whatsnewContainer,
|
.whatsnewContainer,
|
||||||
.Message__Message___3oJaU {
|
[class*="Message__Message___"] {
|
||||||
backdrop-filter: blur(50px);
|
backdrop-filter: blur(50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
@@ -3,6 +3,12 @@ declare module '*.woff';
|
|||||||
declare module '*.scss';
|
declare module '*.scss';
|
||||||
declare module '*.png';
|
declare module '*.png';
|
||||||
declare module '*.html';
|
declare module '*.html';
|
||||||
|
declare module '*.svelte';
|
||||||
|
|
||||||
|
declare module '*?inlineWorker' {
|
||||||
|
const value: () => Worker;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "*.png?base64" {
|
declare module "*.png?base64" {
|
||||||
const value: string;
|
const value: string;
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import React, { createContext, ReactNode, useContext, useState } from 'react';
|
|
||||||
import { SettingsState } from './types/AppProps';
|
|
||||||
import useSettingsState from './hooks/settingsState';
|
|
||||||
|
|
||||||
// Create a context with an initial state
|
|
||||||
const SettingsContext = createContext<{
|
|
||||||
settingsState: SettingsState;
|
|
||||||
setSettingsState: React.Dispatch<React.SetStateAction<SettingsState>>;
|
|
||||||
showPicker: boolean;
|
|
||||||
setShowPicker: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
standalone: boolean;
|
|
||||||
setStandalone: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
} | undefined>(undefined);
|
|
||||||
|
|
||||||
export const SettingsContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
||||||
const [settingsState, setSettingsState] = useState<SettingsState>({
|
|
||||||
notificationCollector: false,
|
|
||||||
lessonAlerts: false,
|
|
||||||
animatedBackground: false,
|
|
||||||
animatedBackgroundSpeed: "0",
|
|
||||||
customThemeColor: "rgba(219, 105, 105, 1)",
|
|
||||||
betterSEQTAPlus: true,
|
|
||||||
shortcuts: [],
|
|
||||||
customshortcuts: [],
|
|
||||||
transparencyEffects: false,
|
|
||||||
selectedTheme: '',
|
|
||||||
animations: true,
|
|
||||||
defaultPage: 'home',
|
|
||||||
devMode: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const [showPicker, setShowPicker] = useState<boolean>(false);
|
|
||||||
const [standalone, setStandalone] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useSettingsState({ settingsState, setSettingsState });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsContext.Provider value={{ settingsState, setSettingsState, showPicker, setShowPicker, standalone, setStandalone }}>
|
|
||||||
{children}
|
|
||||||
</SettingsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
export const useSettingsContext = () => {
|
|
||||||
const context = useContext(SettingsContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useSettingsContext must be used within a SettingsContextProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
const presetBackgrounds = [
|
|
||||||
// Images
|
|
||||||
{
|
|
||||||
id: 'image-preset-1',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-1.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-1-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-preset-2',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-2.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-2-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-preset-3',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-3.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-3-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-preset-4',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-4.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-4-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-preset-5',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-5.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-5-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-preset-6',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-6.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-6-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'image-preset-7',
|
|
||||||
type: 'image',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-7.jpg',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-7-thumb.jpg',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// Videos
|
|
||||||
{
|
|
||||||
id: 'video-preset-1',
|
|
||||||
type: 'video',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animated-1.mp4',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animation-1-thumb.mp4',
|
|
||||||
isPreset: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'video-preset-2',
|
|
||||||
type: 'video',
|
|
||||||
url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animation-2.mp4',
|
|
||||||
previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animation-2-thumb.mp4',
|
|
||||||
isPreset: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default presetBackgrounds;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,32 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
|
||||||
|
|
||||||
const Accordion = ({ children, title, defaultOpened }: { children: React.ReactNode, title: string, defaultOpened?: boolean }) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [shown, setShown] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const show = async () => {
|
|
||||||
if (defaultOpened) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
setShown(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
show();
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => setShown(!shown)} className='flex items-center justify-between text-[15px] w-full'>
|
|
||||||
{ title }
|
|
||||||
<ChevronDownIcon className={`transition-transform duration-300 ${shown ? 'rotate-180' : ''}`} height='24' aria-hidden />
|
|
||||||
</button>
|
|
||||||
<div ref={ref} className='overflow-y-hidden transition-all duration-300 ease-in-out' style={{ height: `${shown ? ref.current?.scrollHeight : '0'}px` }}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Accordion;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
@keyframes shake {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: rotate(-1deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: rotate(1deg);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: rotate(-1deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-shake {
|
|
||||||
animation: shake 0.5s linear infinite;
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
import { ChangeEvent, memo, useEffect, useState } from "react";
|
|
||||||
import { downloadPresetBackground, openDB, readAllData, writeData } from "../hooks/BackgroundDataLoader";
|
|
||||||
import presetBackgrounds from "../assets/presetBackgrounds";
|
|
||||||
import "./BackgroundSelector.css";
|
|
||||||
|
|
||||||
export interface Background {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
blob: Blob;
|
|
||||||
url?: string;
|
|
||||||
previewUrl?: string;
|
|
||||||
isPreset?: boolean;
|
|
||||||
isDownloaded?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BackgroundSelectorProps {
|
|
||||||
isEditMode: boolean;
|
|
||||||
disableTheme: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GetTheme() {
|
|
||||||
return localStorage.getItem('selectedBackground');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function SetTheme(theme: string) {
|
|
||||||
localStorage.setItem('selectedBackground', theme);
|
|
||||||
//await browser.storage.local.set({ theme });
|
|
||||||
}
|
|
||||||
|
|
||||||
function BackgroundSelector({ isEditMode, disableTheme }: BackgroundSelectorProps) {
|
|
||||||
const [backgrounds, setBackgrounds] = useState<Background[]>([]);
|
|
||||||
const [selectedBackground, setSelectedBackground] = useState<string | null>();
|
|
||||||
const [downloadedPresetIds, setDownloadedPresetIds] = useState<string[]>([]);
|
|
||||||
const [downloadProgress, setDownloadProgress] = useState<Record<string, number>>({});
|
|
||||||
|
|
||||||
const [BackgroundsBlocked, setBackgroundsBlocked] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
GetTheme().then((theme) => {
|
|
||||||
setSelectedBackground(theme);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>): Promise<void> => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const fileId = `${Date.now()}-${file.name}`;
|
|
||||||
const fileType = file.type.split('/')[0];
|
|
||||||
const blob = new Blob([file], { type: file.type });
|
|
||||||
|
|
||||||
await writeData(fileId, fileType, blob);
|
|
||||||
setBackgrounds(prev => [...prev, { id: fileId, type: fileType, blob, url: URL.createObjectURL(blob) }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadBackgrounds = async (): Promise<void> => {
|
|
||||||
const data = await readAllData();
|
|
||||||
const dataWithUrls = data.map(bg => ({ ...bg, url: URL.createObjectURL(bg.blob) }));
|
|
||||||
|
|
||||||
// Update downloaded preset IDs
|
|
||||||
setDownloadedPresetIds(data.map(bg => bg.id));
|
|
||||||
|
|
||||||
setBackgrounds(dataWithUrls);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePresetClick = async (bg: Background): Promise<void> => {
|
|
||||||
if (bg.isPreset) {
|
|
||||||
// Check if indexed DB is accessible or whether cross site cookies blocks it
|
|
||||||
try {
|
|
||||||
await openDB();
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-expect-error - Brave is not in the navigator type (unless you are actually using brave browser)
|
|
||||||
if (navigator.brave && await navigator.brave.isBrave() || false) {
|
|
||||||
console.error('[BetterSEQTA+] Brave browser is blocking access to IndexedDB. Please disable the "Cross-site cookies blocked" setting in the Shields panel. (or you can just disable brave shields for SEQTA)');
|
|
||||||
setBackgroundsBlocked(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
alert("[BetterSEQTA+] IndexedDB is not accessible. Please check your browser settings (It's probably cross-site cookies that are blocked).");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already exists in IndexedDB or is currently being downloaded
|
|
||||||
const existingBackgrounds = await readAllData();
|
|
||||||
const alreadyExists = existingBackgrounds.some(ebg => ebg.id === bg.id) || downloadProgress[bg.id] !== undefined;
|
|
||||||
|
|
||||||
if (!alreadyExists) {
|
|
||||||
setDownloadProgress(prev => ({ ...prev, [bg.id]: 0 }));
|
|
||||||
const downloadedBg = await downloadPresetBackground(bg, progress => {
|
|
||||||
setDownloadProgress(prev => ({ ...prev, [bg.id]: progress }));
|
|
||||||
});
|
|
||||||
setDownloadProgress(prev => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { [bg.id]: _, ...rest } = prev;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
await writeData(downloadedBg.id, downloadedBg.type, downloadedBg.blob);
|
|
||||||
setBackgrounds(prev => [...prev, downloadedBg]);
|
|
||||||
setDownloadedPresetIds(prev => [...prev, downloadedBg.id]);
|
|
||||||
}
|
|
||||||
selectBackground(bg.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectBackground = (fileId: string): void => {
|
|
||||||
if (selectedBackground == fileId) {
|
|
||||||
selectNoBackground();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedBackground(fileId);
|
|
||||||
SetTheme(fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteBackground = async (fileId: string): Promise<void> => {
|
|
||||||
const db = await openDB();
|
|
||||||
const tx = db.transaction('backgrounds', 'readwrite');
|
|
||||||
const store = tx.objectStore('backgrounds');
|
|
||||||
store.delete(fileId);
|
|
||||||
setBackgrounds(prev => prev.filter(bg => bg.id !== fileId));
|
|
||||||
|
|
||||||
// Check if the background being deleted is currently selected
|
|
||||||
if (fileId === selectedBackground) {
|
|
||||||
selectNoBackground(); // Disable the current background
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectNoBackground = (): void => {
|
|
||||||
setSelectedBackground(null);
|
|
||||||
SetTheme('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const calcCircumference = (radius: number) => 2 * Math.PI * radius;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadBackgrounds();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
disabled={selectedBackground == null ? true : false}
|
|
||||||
className={`w-full px-4 py-2 mb-4 dark:text-white transition ${selectedBackground == null ? 'dark:bg-zinc-900 bg-zinc-100' : 'bg-blue-500 text-white'} rounded`}
|
|
||||||
onClick={() => { disableTheme(), selectNoBackground() }}>
|
|
||||||
{selectedBackground == null ? 'No Theme' : 'Remove Theme'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{BackgroundsBlocked && (
|
|
||||||
<div className="p-4 mb-4 text-red-600 bg-red-100 rounded-md dark:text-red-300 dark:bg-red-500 dark:bg-opacity-20">
|
|
||||||
<h2 className="mb-2 text-lg font-bold">File Storage Blocked</h2>
|
|
||||||
<p>Brave browser is blocking access to IndexedDB. Please disable the "Cross-site cookies blocked" setting in the Shields panel. (or you can just disable brave shields for SEQTA)</p>
|
|
||||||
<img src="https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/brave.jpg" alt="Brave browser logo" className="w-1/2 mt-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative px-1">
|
|
||||||
<h2 className="pb-2 text-lg font-bold">Background Images</h2>
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
{ isEditMode ? <></> :
|
|
||||||
<div className="relative w-16 h-16 overflow-hidden transition rounded-xl bg-zinc-100 dark:bg-zinc-900">
|
|
||||||
<div className="flex items-center justify-center w-full h-full text-3xl font-bold text-gray-400 transition font-IconFamily hover:text-gray-500">
|
|
||||||
{/* Plus icon */}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<input type="file" accept='image/*, video/*' onChange={handleFileChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
|
||||||
</div>}
|
|
||||||
{backgrounds.filter(bg => bg.type === 'image').map(bg => (
|
|
||||||
<div key={bg.id}
|
|
||||||
onClick={() => selectBackground(bg.id)}
|
|
||||||
className={`relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 ${isEditMode ? 'animate-shake' : ''} ${selectedBackground === bg.id ? 'dark:ring-2 ring-4' : 'ring-0'}`}>
|
|
||||||
{isEditMode && (
|
|
||||||
<div className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
|
||||||
onClick={() => deleteBackground(bg.id)}>
|
|
||||||
<div className="w-4 h-0.5 bg-white"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<img className="object-cover w-full h-full rounded-xl" src={bg.url} alt="swatch" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'image' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => (
|
|
||||||
<button key={bg.id}
|
|
||||||
onClick={() => handlePresetClick(bg)}
|
|
||||||
className={`relative w-16 h-16 transition cursor-pointer rounded-xl duration-300 ${ isEditMode ? 'opacity-0 pointer-events-none hidden' : 'opacity-100'}`}>
|
|
||||||
{bg.isPreset && downloadProgress[bg.id] !== undefined && (
|
|
||||||
<div className="absolute top-0 left-0 z-20 flex items-center justify-center w-full h-full">
|
|
||||||
<svg className="w-full h-full text-zinc-100 dark:text-zinc-700" viewBox="0 0 36 36">
|
|
||||||
<circle stroke="currentColor" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset="0" transform="rotate(-90 18 18)"></circle>
|
|
||||||
<circle stroke="#3B82F6" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset={`${calcCircumference(14) * (1 - (downloadProgress[bg.id] / 100))}`} transform="rotate(-90 18 18)"></circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={`relative transition top-0 z-10 flex justify-center w-full h-full text-white rounded-xl group place-items-center ${downloadProgress[bg.id] === undefined ? 'hover:bg-black/20' : ''}`}>
|
|
||||||
<span className="absolute z-10 text-3xl transition opacity-0 font-IconFamily group-hover:opacity-100">
|
|
||||||
{downloadProgress[bg.id] === undefined ? '' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
className="absolute top-0 object-cover w-full h-full rounded-xl"
|
|
||||||
src={bg.isPreset ? bg.previewUrl : bg.url}
|
|
||||||
alt="swatch" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="py-2 text-lg font-bold">Background Videos</h2>
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
{ isEditMode ? <></> :
|
|
||||||
<div className="relative w-16 h-16 overflow-hidden transition rounded-xl bg-zinc-100 dark:bg-zinc-900">
|
|
||||||
<div className="flex items-center justify-center w-full h-full text-3xl font-bold text-gray-400 transition font-IconFamily hover:text-gray-500">
|
|
||||||
{/* Plus icon */}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<input type="file" accept='image/*, video/*' onChange={handleFileChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{backgrounds.filter(bg => bg.type === 'video').map(bg => (
|
|
||||||
<div key={bg.id} onClick={() => selectBackground(bg.id)} className={`relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 ${isEditMode ? 'animate-shake' : ''} ${selectedBackground === bg.id ? 'dark:ring-2 ring-4' : 'ring-0'}`}>
|
|
||||||
{isEditMode && (
|
|
||||||
<div className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
|
||||||
onClick={() => deleteBackground(bg.id)}>
|
|
||||||
<div className="w-4 h-0.5 bg-white"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<video muted loop autoPlay src={bg.url} className="object-cover w-full h-full rounded-xl" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'video' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => (
|
|
||||||
<div key={bg.id}
|
|
||||||
onClick={() => handlePresetClick(bg)}
|
|
||||||
className={`relative w-16 h-16 transition cursor-pointer rounded-xl duration-300 ${ isEditMode ? 'opacity-0 pointer-events-none hidden' : 'opacity-100'}`}>
|
|
||||||
{bg.isPreset && downloadProgress[bg.id] !== undefined && (
|
|
||||||
<div className="absolute top-0 left-0 z-20 flex items-center justify-center w-full h-full">
|
|
||||||
<svg className="w-full h-full text-zinc-100 dark:text-zinc-700" viewBox="0 0 36 36">
|
|
||||||
<circle stroke="currentColor" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset="0" transform="rotate(-90 18 18)"></circle>
|
|
||||||
<circle stroke="#3B82F6" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset={`${calcCircumference(14) * (1 - (downloadProgress[bg.id] / 100))}`} transform="rotate(-90 18 18)"></circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={`relative transition top-0 z-10 flex justify-center w-full h-full text-white rounded-xl group place-items-center ${downloadProgress[bg.id] === undefined ? 'hover:bg-black/20' : ''}`}>
|
|
||||||
<span className="absolute z-10 text-3xl transition opacity-0 font-IconFamily group-hover:opacity-100">
|
|
||||||
{downloadProgress[bg.id] === undefined ? '' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<video muted loop autoPlay src={bg.isPreset ? bg.previewUrl : bg.url} className="absolute top-0 object-cover w-full h-full rounded-xl" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(BackgroundSelector);
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { onClick, text } = $props<{ onClick: () => void, text: string, [key: string]: any }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={onClick} class='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
type CheckboxProps = {
|
|
||||||
value: boolean;
|
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Checkbox: React.FC<CheckboxProps> = ({ value, onChange }) => {
|
|
||||||
return (
|
|
||||||
<label className="flex items-center cursor-pointer">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="absolute opacity-0"
|
|
||||||
checked={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`w-5 h-5 rounded-md bg-gradient-to-tr transition-colors duration-200 ${
|
|
||||||
value
|
|
||||||
? 'from-blue-500 to-blue-600'
|
|
||||||
: 'from-gray-300 to-gray-400 dark:from-zinc-700 dark:to-zinc-700/50'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{value && (
|
|
||||||
<svg
|
|
||||||
className="absolute inset-0 m-auto text-white"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="20 6 9 17 4 12" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Checkbox;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.cm-editor {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.dark) .cm-editor {
|
|
||||||
@apply bg-zinc-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor.cm-focused {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||||
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { indentWithTab, history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||||
|
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||||
|
import { highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, EditorView, dropCursor } from '@codemirror/view';
|
||||||
|
import { color } from '@uiw/codemirror-extensions-color'
|
||||||
|
import { Compartment } from '@codemirror/state';
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
import { githubLight, githubDark } from '@uiw/codemirror-theme-github';
|
||||||
|
|
||||||
|
// Language
|
||||||
|
import { css } from "@codemirror/lang-css";
|
||||||
|
|
||||||
|
let editor = $state<HTMLDivElement | null>(null)
|
||||||
|
let view: EditorView | null = null;
|
||||||
|
let editorTheme = new Compartment();
|
||||||
|
let { value, onChange, className } = $props<{value: string, onChange: (value: string) => void, className?: string}>()
|
||||||
|
|
||||||
|
function createEditorState(initialContents: string) {
|
||||||
|
let extensions = [
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
drawSelection(),
|
||||||
|
indentUnit.of(" "),
|
||||||
|
EditorState.allowMultipleSelections.of(true),
|
||||||
|
indentOnInput(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
autocompletion(),
|
||||||
|
rectangularSelection(),
|
||||||
|
crosshairCursor(),
|
||||||
|
dropCursor(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
highlightSelectionMatches(),
|
||||||
|
editorTheme.of(githubLight),
|
||||||
|
keymap.of([
|
||||||
|
indentWithTab,
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...foldKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
]),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
onChange(update.state.doc.toString())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
css(),
|
||||||
|
color,
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
];
|
||||||
|
|
||||||
|
return EditorState.create({
|
||||||
|
doc: initialContents,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEditorView(state: EditorState, parent: HTMLElement) {
|
||||||
|
return new EditorView({ state, parent });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (editor) {
|
||||||
|
const state = createEditorState(value);
|
||||||
|
view = createEditorView(state, editor as HTMLElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsState.subscribe((settings) => {
|
||||||
|
if (view) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: editorTheme.reconfigure(
|
||||||
|
settings.DarkMode ? githubDark : githubLight
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (view) {
|
||||||
|
view.destroy();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={`rounded-lg text-[13px] overflow-clip w-full bg-white dark:bg-zinc-900 ${className}`} bind:this={editor}></div>
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import CodeMirror, { ViewUpdate } from '@uiw/react-codemirror'
|
|
||||||
import { githubDark, githubLight } from '@uiw/codemirror-theme-github'
|
|
||||||
import { color } from '@uiw/codemirror-extensions-color';
|
|
||||||
import { less } from '@codemirror/lang-less'
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import './CodeEditor.css'
|
|
||||||
|
|
||||||
export default function CodeEditor({
|
|
||||||
className = '',
|
|
||||||
height = '100%',
|
|
||||||
value,
|
|
||||||
setValue
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
height?: string;
|
|
||||||
value: string;
|
|
||||||
setValue: (value: string) => void;
|
|
||||||
}) {
|
|
||||||
const [darkMode, setDarkMode] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
|
||||||
setDarkMode(true)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onChange = useCallback((value: string, _: ViewUpdate) => {
|
|
||||||
setValue(value)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return(
|
|
||||||
<CodeMirror
|
|
||||||
basicSetup={{
|
|
||||||
allowMultipleSelections: true,
|
|
||||||
lineNumbers: false,
|
|
||||||
foldGutter: false,
|
|
||||||
dropCursor: true,
|
|
||||||
tabSize: 2,
|
|
||||||
}}
|
|
||||||
theme={ darkMode ? githubDark : githubLight }
|
|
||||||
placeholder={"Happy coding!"}
|
|
||||||
className={`rounded-lg text-[13px] ${className}`}
|
|
||||||
value={value}
|
|
||||||
height={height}
|
|
||||||
extensions={[less(), color]}
|
|
||||||
onChange={onChange} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
div:has(> #rbgcp-wrapper) {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
#rbgcp-wrapper {
|
||||||
|
div[style="padding-top: 11px; position: relative;"] div {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rbgcp-inputs-wrap #rbgcp-hex-input,
|
||||||
|
#rbgcp-inputs-wrap #rbgcp-input {
|
||||||
|
color: white !important;
|
||||||
|
background-color: #37373b !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:has(> #rbgcp-solid-btn),
|
||||||
|
div:has(> #rbgcp-advanced-btn),
|
||||||
|
#rbgcp-color-model-btn > div,
|
||||||
|
#rbgcp-gradient-controls-wrap {
|
||||||
|
background-color: #37373b !important;
|
||||||
|
color: white !important;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
circle {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
polyline,
|
||||||
|
line,
|
||||||
|
g,
|
||||||
|
path {
|
||||||
|
stroke: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#rbgcp-radial-btn,
|
||||||
|
#rbgcp-linear-btn {
|
||||||
|
&[style*="background: white;"] {
|
||||||
|
background-color: #28282b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
path,
|
||||||
|
g,
|
||||||
|
polyline,
|
||||||
|
circle {
|
||||||
|
stroke: white !important;
|
||||||
|
fill: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div:has(> #rbgcp-stop-input) svg {
|
||||||
|
path {
|
||||||
|
stroke: unset !important;
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#rbgcp-comparibles-btn svg path {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
color: white !important;
|
||||||
|
|
||||||
|
&[style*="background: white;"] {
|
||||||
|
background: #28282b !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div:has(> #rbgcp-degree-input) {
|
||||||
|
width: 70px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rbgcp-degree-input {
|
||||||
|
width: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rbgcp-degree-input,
|
||||||
|
#rbgcp-stop-input {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rbgcp-gradient-controls-wrap > div,
|
||||||
|
#rbgcp-gradient-controls-wrap > div > div:not([role="button"]) {
|
||||||
|
background-color: #37373b !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import ColourPicker from './ColourPicker.tsx';
|
||||||
|
import ReactAdapter from './utils/ReactAdapter.svelte';
|
||||||
|
import { animate } from 'motion';
|
||||||
|
import { delay } from '@/seqta/utils/delay.ts'
|
||||||
|
|
||||||
|
const { hidePicker, standalone = false, savePresets = true, customOnChange = null, customState = null } = $props<{
|
||||||
|
hidePicker?: () => void,
|
||||||
|
standalone?: boolean,
|
||||||
|
savePresets?: boolean,
|
||||||
|
customOnChange?: (color: string) => void,
|
||||||
|
customState?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let background = $state<HTMLDivElement | null>(null);
|
||||||
|
let content = $state<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const closePicker = async () => {
|
||||||
|
if (standalone) return;
|
||||||
|
if (!background || !content) return;
|
||||||
|
|
||||||
|
animate(
|
||||||
|
content,
|
||||||
|
{ scale: [1, 0.4], opacity: [1, 0] },
|
||||||
|
{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 30
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
animate(
|
||||||
|
background,
|
||||||
|
{ opacity: [1, 0] },
|
||||||
|
{ ease: [0.4, 0, 0.2, 1] }
|
||||||
|
);
|
||||||
|
|
||||||
|
await delay(400);
|
||||||
|
hidePicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (standalone) return;
|
||||||
|
if (!background || !content) return;
|
||||||
|
|
||||||
|
animate(
|
||||||
|
background,
|
||||||
|
{ opacity: [0, 1] },
|
||||||
|
{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }
|
||||||
|
);
|
||||||
|
|
||||||
|
animate(
|
||||||
|
content,
|
||||||
|
{ scale: [0.4, 1], opacity: [0, 1] },
|
||||||
|
{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 30
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEscapeKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closePicker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscapeKey);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleEscapeKey);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleBackgroundClick(event: MouseEvent) {
|
||||||
|
if (event.target === background) {
|
||||||
|
closePicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if standalone}
|
||||||
|
<div class="h-auto rounded-xl overflow-clip">
|
||||||
|
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={background}
|
||||||
|
class="absolute top-0 left-0 z-50 flex items-center justify-center w-full h-full cursor-pointer bg-black/20"
|
||||||
|
onclick={handleBackgroundClick}
|
||||||
|
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bind:this={content}
|
||||||
|
class="h-auto p-4 bg-white border shadow-lg cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||||
|
>
|
||||||
|
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import ColorPicker from "react-best-gradient-color-picker"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||||
|
|
||||||
|
const defaultPresets = [
|
||||||
|
"linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(0, 141, 201, 0.76) 0%, rgba(8, 5, 170, 0.66) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(0, 201, 20, 0.76) 0%, rgba(4, 160, 105, 0.66) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(199, 20, 55, 0.76) 0%, rgba(95, 11, 160, 0.66) 100%)",
|
||||||
|
"linear-gradient(40deg, rgba(24, 20, 199, 0.76) 0%, rgba(23, 173, 65, 0.66) 100%)",
|
||||||
|
"radial-gradient(circle, rgba(20, 199, 178, 0.76) 32%, rgba(3, 120, 57, 0.66) 100%)",
|
||||||
|
"radial-gradient(circle, rgba(13, 15, 145, 0.76) 12%, rgba(103, 3, 120, 0.66) 100%)",
|
||||||
|
"linear-gradient(20deg, rgb(230, 21, 21) 0%, rgb(230, 109, 21) 12%, rgb(230, 34, 21) 26%, rgb(230, 21, 21) 39%, rgb(230, 84, 21) 48%, rgb(230, 34, 21) 58%, rgb(230, 96, 21) 69%, rgb(230, 34, 21) 80%, rgb(230, 71, 21) 89%, rgb(230, 21, 21) 100%)",
|
||||||
|
"rgba(114, 1, 170, 0.89)",
|
||||||
|
"rgba(93, 135, 63, 0.89)",
|
||||||
|
"rgba(4, 4, 138, 0.77)",
|
||||||
|
"rgba(21, 20, 20, 0.89)",
|
||||||
|
"linear-gradient(340deg, rgb(205, 74, 82) 18%, rgba(132, 8, 8, 0.89) 46%, rgb(204, 78, 85) 72%)",
|
||||||
|
"radial-gradient(circle, rgb(74, 205, 158) 0%, rgba(8, 72, 132, 0.89) 99%)",
|
||||||
|
"rgba(17, 94, 89, 1)",
|
||||||
|
"rgba(30, 64, 175, 0.89)",
|
||||||
|
"rgba(134, 25, 143, 1)",
|
||||||
|
"rgba(14, 165, 233, 0.9)",
|
||||||
|
]
|
||||||
|
|
||||||
|
interface PickerProps {
|
||||||
|
customOnChange?: (color: string) => void
|
||||||
|
customState?: string
|
||||||
|
savePresets?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Picker({
|
||||||
|
customOnChange,
|
||||||
|
customState,
|
||||||
|
savePresets = true,
|
||||||
|
}: PickerProps) {
|
||||||
|
const [customThemeColor, setCustomThemeColor] = useState<string | null>()
|
||||||
|
const [presets, setPresets] = useState<string[]>()
|
||||||
|
|
||||||
|
const latestValuesRef = useRef({ customThemeColor, customOnChange, savePresets, presets });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customState !== undefined && customState !== null) {
|
||||||
|
setCustomThemeColor(customState)
|
||||||
|
} else {
|
||||||
|
setCustomThemeColor(settingsState.selectedColor ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presets === undefined) {
|
||||||
|
const savedPresets = localStorage.getItem("colorPickerPresets")
|
||||||
|
setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestValuesRef.current = { customThemeColor, customOnChange, savePresets, presets };
|
||||||
|
}, [customThemeColor, customOnChange, savePresets, presets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const { customThemeColor, customOnChange, savePresets, presets } = latestValuesRef.current;
|
||||||
|
if (!(customThemeColor && !customOnChange && savePresets && presets)) return;
|
||||||
|
|
||||||
|
// Only proceed if presets are different (avoid unnecessary updates)
|
||||||
|
const existingIndex = presets.indexOf(customThemeColor);
|
||||||
|
let updatedPresets;
|
||||||
|
|
||||||
|
if (existingIndex === 0) {
|
||||||
|
// No need to update if the selected color is already the first element
|
||||||
|
return;
|
||||||
|
} else if (existingIndex > -1) {
|
||||||
|
updatedPresets = [
|
||||||
|
customThemeColor,
|
||||||
|
...presets.slice(0, existingIndex),
|
||||||
|
...presets.slice(existingIndex + 1),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
updatedPresets = [customThemeColor, ...presets].slice(0, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("colorPickerPresets", JSON.stringify(updatedPresets));
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customThemeColor && !customOnChange) {
|
||||||
|
settingsState.selectedColor = customThemeColor
|
||||||
|
}
|
||||||
|
}, [customThemeColor, customOnChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
disableDarkMode={true}
|
||||||
|
presets={presets}
|
||||||
|
hideInputs={customOnChange ? false : true}
|
||||||
|
value={customThemeColor ?? ""}
|
||||||
|
onChange={(color: string) => {
|
||||||
|
if (customOnChange) {
|
||||||
|
customOnChange(color)
|
||||||
|
setCustomThemeColor(color)
|
||||||
|
} else {
|
||||||
|
setCustomThemeColor(color)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const SpinnerIcon = ({ className }: { className: string }) => (
|
|
||||||
<svg className={className} width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>{`.spinner_7mtw{transform-origin:center;animation:spinner_jgYN .6s linear infinite}@keyframes spinner_jgYN{100%{transform:rotate(360deg)}}`}</style>
|
|
||||||
<path stroke="currentColor" fill="currentColor" className="spinner_7mtw" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default SpinnerIcon;
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { animate as motionAnimate } from 'motion';
|
||||||
|
|
||||||
|
let { initial, animate, exit, transition, children, class: className } = $props<{
|
||||||
|
initial?: any,
|
||||||
|
animate?: any,
|
||||||
|
exit?: any,
|
||||||
|
transition?: any,
|
||||||
|
children?: any,
|
||||||
|
class?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let divElement: HTMLElement;
|
||||||
|
|
||||||
|
const playAnimation = (keyframe: any) => {
|
||||||
|
if (divElement && keyframe) {
|
||||||
|
let finalKeyframe = { ...keyframe };
|
||||||
|
|
||||||
|
if (finalKeyframe.height === 'auto') {
|
||||||
|
const prevHeight = divElement.style.height;
|
||||||
|
const prevVisibility = divElement.style.visibility;
|
||||||
|
|
||||||
|
divElement.style.height = 'auto';
|
||||||
|
divElement.style.visibility = 'hidden';
|
||||||
|
divElement.style.position = 'absolute';
|
||||||
|
|
||||||
|
const autoHeight = divElement.offsetHeight;
|
||||||
|
|
||||||
|
divElement.style.height = prevHeight;
|
||||||
|
divElement.style.visibility = prevVisibility;
|
||||||
|
divElement.style.position = '';
|
||||||
|
|
||||||
|
finalKeyframe.height = `${autoHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSpringConfig = { stiffness: 250, damping: 25 };
|
||||||
|
|
||||||
|
const animation = motionAnimate(
|
||||||
|
[divElement],
|
||||||
|
finalKeyframe,
|
||||||
|
{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: transition?.stiffness || defaultSpringConfig.stiffness,
|
||||||
|
damping: transition?.damping || defaultSpringConfig.damping
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (initial) {
|
||||||
|
Object.assign(divElement.style, initial);
|
||||||
|
await playAnimation(animate || {});
|
||||||
|
} else if (animate) {
|
||||||
|
await playAnimation(animate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (animate) {
|
||||||
|
playAnimation(animate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(async () => {
|
||||||
|
if (exit) {
|
||||||
|
await playAnimation(exit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={className} bind:this={divElement} style="will-change: transform, opacity;">
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.dark [class*="rbgcpColorModelDropdown"],
|
|
||||||
.dark [class*="rbgcpControlBtnWrapper"],
|
|
||||||
.dark #rbgcp-gradient-controls-wrap {
|
|
||||||
background-color: #37373b !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark [class*="rbgcpControlBtn"][class*="rbgcpControlBtnSelected"] {
|
|
||||||
color: #568cf5 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark [class*="rbgcpControlBtn"] {
|
|
||||||
color: #CDCEC9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark [class*="rbgcpControlBtnSelected"] svg {
|
|
||||||
filter: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark [class*="rbgcpControlBtnSelected"] {
|
|
||||||
background-color: #28282b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark [class*="rbgcpComparibleLabel"] {
|
|
||||||
color: #CDCEC9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark #rbgcp-stop-input,
|
|
||||||
.dark #rbgcp-degree-input,
|
|
||||||
.dark [class*="rbgcpControlBtnWrapper"] svg {
|
|
||||||
filter: invert();
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import ColorPicker from 'react-best-gradient-color-picker';
|
|
||||||
import { useSettingsContext } from '../SettingsContext';
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
import "./Picker.css";
|
|
||||||
import { memo, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
function Picker() {
|
|
||||||
const { settingsState, setSettingsState, showPicker, setShowPicker } = useSettingsContext();
|
|
||||||
|
|
||||||
const defaultPresets = [
|
|
||||||
'linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)',
|
|
||||||
'linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)',
|
|
||||||
'linear-gradient(40deg, rgba(0, 141, 201, 0.76) 0%, rgba(8, 5, 170, 0.66) 100%)',
|
|
||||||
'linear-gradient(40deg, rgba(0, 201, 20, 0.76) 0%, rgba(4, 160, 105, 0.66) 100%)',
|
|
||||||
'linear-gradient(40deg, rgba(199, 20, 55, 0.76) 0%, rgba(95, 11, 160, 0.66) 100%)',
|
|
||||||
'linear-gradient(40deg, rgba(24, 20, 199, 0.76) 0%, rgba(23, 173, 65, 0.66) 100%)',
|
|
||||||
'radial-gradient(circle, rgba(20, 199, 178, 0.76) 32%, rgba(3, 120, 57, 0.66) 100%)',
|
|
||||||
'radial-gradient(circle, rgba(13, 15, 145, 0.76) 12%, rgba(103, 3, 120, 0.66) 100%)',
|
|
||||||
'linear-gradient(20deg, rgb(230, 21, 21) 0%, rgb(230, 109, 21) 12%, rgb(230, 34, 21) 26%, rgb(230, 21, 21) 39%, rgb(230, 84, 21) 48%, rgb(230, 34, 21) 58%, rgb(230, 96, 21) 69%, rgb(230, 34, 21) 80%, rgb(230, 71, 21) 89%, rgb(230, 21, 21) 100%)',
|
|
||||||
'rgba(114, 1, 170, 0.89)',
|
|
||||||
'rgba(93, 135, 63, 0.89)',
|
|
||||||
'rgba(4, 4, 138, 0.77)',
|
|
||||||
'rgba(21, 20, 20, 0.89)',
|
|
||||||
'linear-gradient(340deg, rgb(205, 74, 82) 18%, rgba(132, 8, 8, 0.89) 46%, rgb(204, 78, 85) 72%)',
|
|
||||||
'radial-gradient(circle, rgb(74, 205, 158) 0%, rgba(8, 72, 132, 0.89) 99%)',
|
|
||||||
'rgba(17, 94, 89, 1)',
|
|
||||||
'rgba(30, 64, 175, 0.89)',
|
|
||||||
'rgba(134, 25, 143, 1)',
|
|
||||||
'rgba(14, 165, 233, 0.9)'
|
|
||||||
];
|
|
||||||
const [presets, setPresets] = useState(() => {
|
|
||||||
const savedPresets = localStorage.getItem('colorPickerPresets');
|
|
||||||
return savedPresets ? JSON.parse(savedPresets) : defaultPresets;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleMessage = (event: MessageEvent) => {
|
|
||||||
if (event.data === "popupClosed") {
|
|
||||||
setShowPicker(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Add event listener for 'message' event
|
|
||||||
window.addEventListener("message", handleMessage);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("message", handleMessage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Watch for changes in showPicker and update the presets
|
|
||||||
if (!showPicker) {
|
|
||||||
// Check if the selected color is already in the presets
|
|
||||||
const existingIndex = presets.indexOf(settingsState.customThemeColor);
|
|
||||||
|
|
||||||
let updatedPresets;
|
|
||||||
if (existingIndex > -1) {
|
|
||||||
// If the color exists, move it to the front
|
|
||||||
updatedPresets = [
|
|
||||||
settingsState.customThemeColor,
|
|
||||||
...presets.slice(0, existingIndex),
|
|
||||||
...presets.slice(existingIndex + 1)
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// If the color is new, add it to the front and slice the array
|
|
||||||
updatedPresets = [settingsState.customThemeColor, ...presets].slice(0, 18);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPresets(updatedPresets);
|
|
||||||
localStorage.setItem('colorPickerPresets', JSON.stringify(updatedPresets));
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [showPicker]);
|
|
||||||
|
|
||||||
const colorChange = (color: string) => {
|
|
||||||
setSettingsState({
|
|
||||||
...settingsState,
|
|
||||||
customThemeColor: color,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define animation variants
|
|
||||||
const backgroundVariants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: { opacity: 1 },
|
|
||||||
exit: { opacity: 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
const scaleVariants = {
|
|
||||||
hidden: { scale: 0.3 },
|
|
||||||
visible: { scale: 1 },
|
|
||||||
exit: { scale: 0.4 } // Adding exit animation
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Apply fade-in animation to background
|
|
||||||
<motion.div
|
|
||||||
initial="hidden"
|
|
||||||
animate={showPicker ? "visible" : "exit"}
|
|
||||||
exit="exit"
|
|
||||||
variants={backgroundVariants}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
onClick={() => setShowPicker(false)}
|
|
||||||
className={`absolute top-0 left-0 z-50 flex justify-center w-full h-full pt-4 bg-black/20 ${!showPicker ? 'pointer-events-none' : ''}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{/* Apply springy scale animation */}
|
|
||||||
<motion.div
|
|
||||||
initial="hidden"
|
|
||||||
animate={showPicker ? "visible" : "exit"}
|
|
||||||
exit="exit"
|
|
||||||
variants={scaleVariants}
|
|
||||||
transition={{ type: "spring", stiffness: 500, damping: 40 }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="h-auto p-4 bg-white border rounded-lg shadow-lg dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
|
||||||
>
|
|
||||||
<ColorPicker disableDarkMode={true} presets={presets} hideInputs={true} value={settingsState.customThemeColor} onChange={colorChange} />
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Picker);
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||||
|
|
||||||
|
let { onClick } = $props<{ onClick: () => void }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-label="Color Picker Swatch"
|
||||||
|
onclick={onClick}
|
||||||
|
style="background: {$settingsState.selectedColor}"
|
||||||
|
class="w-16 h-8 rounded-md"
|
||||||
|
></button>
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { useSettingsContext } from '../SettingsContext';
|
|
||||||
|
|
||||||
const PickerSwatch = () => {
|
|
||||||
const { setShowPicker, settingsState } = useSettingsContext();
|
|
||||||
|
|
||||||
const enablePicker = () => {
|
|
||||||
setShowPicker(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={enablePicker}
|
|
||||||
style={{ background: settingsState.customThemeColor }}
|
|
||||||
className="w-16 h-8 rounded-md"
|
|
||||||
></button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(PickerSwatch);
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { state, onChange, options } = $props<{
|
||||||
|
state: string,
|
||||||
|
onChange: (newState: string) => void,
|
||||||
|
options: Array<{ value: string, label: string }>
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let select: HTMLSelectElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select
|
||||||
|
bind:this={select}
|
||||||
|
value={state}
|
||||||
|
onchange={() => onChange(select.value)}
|
||||||
|
class="px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md w-full"
|
||||||
|
>
|
||||||
|
{#each options as option}
|
||||||
|
<option value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function Select({ state, onChange, options }: { state: string, onChange: (value: string) => void, options: { value: string, label: string }[] }) {
|
|
||||||
return (
|
|
||||||
<select className='px-4 py-1.5 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white focus:border-none rounded-md mt-2 block w-full border-0 pl-3 pr-10 text-gray-900 focus:outline-none sm:text-sm sm:leading-6' value={state} onChange={(e) => onChange(e.target.value)}>
|
|
||||||
{options.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
|
||||||
</select>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { width, height} = $props<{width?: string, height?: string}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="width: {width ? width : '100%'}; height: {height ? height : '100%'}; background: #e0e0e0;" class="animate-pulse"></div>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/* Slider Thumb */
|
|
||||||
.slider::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider::-moz-range-thumb {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { state, onChange, min = 0, max = 100, step = 1 } = $props<{
|
||||||
|
state: number,
|
||||||
|
onChange: (value: number) => void,
|
||||||
|
min?: number,
|
||||||
|
max?: number,
|
||||||
|
step?: number
|
||||||
|
}>();
|
||||||
|
let percentage = $derived(((state - min) / (max - min)) * 100);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative mx-auto w-full max-w-lg">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
bind:value={state}
|
||||||
|
style={`background: linear-gradient(to right, #30D259 ${percentage}%, #dddddd ${percentage}%)`}
|
||||||
|
onchange={(e) => onChange(Number(e.currentTarget.value))}
|
||||||
|
class="w-full h-1 rounded-full appearance-none cursor-pointer dark:bg-[#38373D] bg-[#DDDDDD] slider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import "./Slider.css";
|
|
||||||
|
|
||||||
interface SliderProps {
|
|
||||||
state: number;
|
|
||||||
onChange: (value: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Slider: React.FC<SliderProps> = ({ state, onChange }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full max-w-lg py-8 mx-auto">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={state}
|
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
|
||||||
className="w-full h-1 rounded-full appearance-none cursor-pointer slider dark:bg-[#38373D] bg-[#DDDDDD]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Slider);
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { size = 'md', color = 'currentColor' } = $props();
|
||||||
|
|
||||||
|
const sizeMap = {
|
||||||
|
sm: '1rem',
|
||||||
|
md: '2rem',
|
||||||
|
lg: '3rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
let dimensions = $derived(sizeMap[size as keyof typeof sizeMap] || size);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class="animate-spin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={dimensions}
|
||||||
|
height={dimensions}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke={color}
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill={color}
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { animate } from 'motion';
|
||||||
|
import { standalone } from '../utils/standalone.svelte'
|
||||||
|
|
||||||
|
let { state, onChange } = $props<{ state: boolean, onChange: (newState: boolean) => void }>();
|
||||||
|
let handle: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const springParams = {
|
||||||
|
stiffness: 600,
|
||||||
|
damping: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateSwitch = (enabled: boolean) => {
|
||||||
|
if (!handle) return;
|
||||||
|
animate(
|
||||||
|
handle,
|
||||||
|
{
|
||||||
|
x: enabled ? (standalone.standalone ? 24 : 20) : 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: springParams.stiffness,
|
||||||
|
damping: springParams.damping,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger animation whenever state changes
|
||||||
|
$effect(() => animateSwitch(state));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex w-14 p-1 cursor-pointer transition-all duration-150 rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch select-none"
|
||||||
|
data-ison={state}
|
||||||
|
onclick={() => onChange(!state)}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && onChange(!state)}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={state}
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bind:this={handle}
|
||||||
|
class="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.switch[data-ison="true"] {
|
||||||
|
background-color: #30D259;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { motion } from "framer-motion";
|
|
||||||
import "./Switch.css";
|
|
||||||
import type { SwitchProps } from "../types/SwitchProps";
|
|
||||||
import { memo } from "react";
|
|
||||||
|
|
||||||
function Switch(props: SwitchProps) {
|
|
||||||
const toggleSwitch = () => {
|
|
||||||
const newIsOn = !props.state;
|
|
||||||
props.onChange(newIsOn);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex w-14 p-1 cursor-pointer rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch"
|
|
||||||
data-ison={props.state}
|
|
||||||
onClick={toggleSwitch}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
|
|
||||||
className="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
|
|
||||||
initial={{ x: props.state ? 0 : 0 }}
|
|
||||||
animate={{ x: props.state ? 24 : 0 }}
|
|
||||||
transition={spring}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const spring = {
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 700,
|
|
||||||
damping: 30
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Switch);
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.tab-width {
|
||||||
|
width: var(--tab-width);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MotionDiv from './MotionDiv.svelte';
|
||||||
|
import './TabbedContainer.css';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
|
||||||
|
let activeTab = $state(0);
|
||||||
|
let containerRef: HTMLElement | null = null;
|
||||||
|
let tabWidth = $state(0);
|
||||||
|
|
||||||
|
const springTransition = { type: 'spring', stiffness: 250, damping: 25 };
|
||||||
|
|
||||||
|
const updateTabWidth = () => {
|
||||||
|
tabWidth = tabs.length > 0 ? 100 / tabs.length : 0;
|
||||||
|
if (!containerRef) return;
|
||||||
|
containerRef.style.setProperty('--tab-width', `${tabWidth}%`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcXPos = (index: number | null) => {
|
||||||
|
if (containerRef) {
|
||||||
|
return tabWidth * (index !== null ? index : activeTab) * containerRef.getBoundingClientRect().width / 100;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateTabWidth();
|
||||||
|
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (event.data === "popupClosed") {
|
||||||
|
activeTab = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("message", handleMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 px-2 tab-width-container">
|
||||||
|
<div bind:this={containerRef} class="flex relative">
|
||||||
|
<MotionDiv
|
||||||
|
class="absolute top-0 left-0 z-0 h-full bg-[#DDDDDD] dark:bg-[#38373D] rounded-full opacity-40 tab-width"
|
||||||
|
animate={{ x: calcXPos(activeTab) }}
|
||||||
|
transition={springTransition}
|
||||||
|
/>
|
||||||
|
{#each tabs as { title }, index}
|
||||||
|
<button
|
||||||
|
class="relative z-10 flex-1 px-4 py-2 focus-visible:outline-none"
|
||||||
|
onclick={() => activeTab = index}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden px-4 h-full">
|
||||||
|
<MotionDiv
|
||||||
|
class="h-full"
|
||||||
|
animate={{ x: `${-activeTab * 100}%` }}
|
||||||
|
transition={springTransition}
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
{#each tabs as { Content, props }, index}
|
||||||
|
<div class="absolute focus:outline-none w-full transition-opacity duration-300 overflow-y-scroll no-scrollbar h-full tab {activeTab === index ? 'opacity-100 active' : 'opacity-0'}"
|
||||||
|
style="left: {index * 100}%;">
|
||||||
|
<Content {...props} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</MotionDiv>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import type { TabbedContainerProps } from '../types/TabbedContainerProps';
|
|
||||||
import { useSettingsContext } from '../SettingsContext';
|
|
||||||
|
|
||||||
const TabbedContainer: React.FC<TabbedContainerProps> = ({ tabs }) => {
|
|
||||||
const { settingsState } = useSettingsContext();
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
|
||||||
const [hoveredTab, setHoveredTab] = useState<number | null>(null);
|
|
||||||
const [tabWidth, setTabWidth] = useState(0);
|
|
||||||
const [position, setPosition] = useState(0);
|
|
||||||
const positionRef = useRef(position);
|
|
||||||
|
|
||||||
// Function to handle message
|
|
||||||
const handleMessage = (event: MessageEvent) => {
|
|
||||||
if (event.data === "popupClosed") {
|
|
||||||
setActiveTab(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Add event listener for 'message' event
|
|
||||||
window.addEventListener("message", handleMessage);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("message", handleMessage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const newPosition = -activeTab * 100;
|
|
||||||
setPosition(newPosition);
|
|
||||||
positionRef.current = newPosition;
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
|
|
||||||
const springTransition = settingsState.animations ? { type: 'spring', stiffness: 250, damping: 25 } : { duration: 0 };
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
// @ts-expect-error for some reason its giving an error in TS but it works...
|
|
||||||
const width = containerRef.current.getBoundingClientRect().width;
|
|
||||||
setTabWidth(width / tabs.length);
|
|
||||||
}
|
|
||||||
}, [tabs.length]);
|
|
||||||
|
|
||||||
const calcXPos = (index: number | null) => {
|
|
||||||
if (index !== null) {
|
|
||||||
return tabWidth * index;
|
|
||||||
}
|
|
||||||
return tabWidth * activeTab;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div ref={containerRef} className="top-0 z-10 text-[0.875rem] pb-0.5 mx-4">
|
|
||||||
<div className="relative flex">
|
|
||||||
<motion.div
|
|
||||||
className="absolute top-0 left-0 z-0 h-full bg-[#DDDDDD] dark:bg-[#38373D] rounded-full opacity-40"
|
|
||||||
style={{ width: `${tabWidth}px` }}
|
|
||||||
initial={false}
|
|
||||||
animate={{ x: calcXPos(hoveredTab) }}
|
|
||||||
transition={springTransition}
|
|
||||||
/>
|
|
||||||
{tabs.map((tab, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
className="relative z-10 flex-1 px-4 py-2"
|
|
||||||
onClick={() => setActiveTab(index)}
|
|
||||||
onMouseEnter={() => setHoveredTab(index)}
|
|
||||||
onMouseLeave={() => setHoveredTab(null)}
|
|
||||||
>
|
|
||||||
{tab.title}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-full px-4 overflow-x-clip">
|
|
||||||
<motion.div
|
|
||||||
initial={false}
|
|
||||||
animate={{ x: `${position}%` }}
|
|
||||||
transition={springTransition}
|
|
||||||
className='flex'
|
|
||||||
>
|
|
||||||
{tabs.map((tab, index) => (
|
|
||||||
<div key={index} className={`absolute h-[100vh] focus-visible:outline-none overflow-y-scroll w-full pb-40 ${ settingsState.animations ? 'transition-opacity duration-300' : ''} ${activeTab === index ? 'opacity-100' : 'opacity-0'}`}
|
|
||||||
style={{left: `${index * 100}%`}}>
|
|
||||||
{tab.content}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(TabbedContainer);
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { CustomTheme, DownloadedTheme } from '../types/CustomThemes';
|
|
||||||
import browser from 'webextension-polyfill';
|
|
||||||
import { ArrowUpOnSquareIcon, PencilIcon } from '@heroicons/react/24/outline';
|
|
||||||
import { sendThemeUpdate, setTheme } from '../hooks/ThemeManagment';
|
|
||||||
import { DeleteDownloadedTheme } from '../pages/Store';
|
|
||||||
|
|
||||||
type ThemeCoverProps = {
|
|
||||||
theme: Omit<CustomTheme, 'CustomImages'> | DownloadedTheme;
|
|
||||||
isSelected: boolean;
|
|
||||||
isEditMode: boolean;
|
|
||||||
downloaded?: boolean;
|
|
||||||
onThemeSelect: (themeId: string) => void;
|
|
||||||
onThemeDelete: (themeId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemeCover: React.FC<ThemeCoverProps> = React.memo(({
|
|
||||||
theme,
|
|
||||||
downloaded,
|
|
||||||
isSelected,
|
|
||||||
isEditMode,
|
|
||||||
onThemeSelect,
|
|
||||||
onThemeDelete,
|
|
||||||
}) => {
|
|
||||||
const [uploading, setUploading] = useState<boolean>(false);
|
|
||||||
const handleThemeClick = async () => {
|
|
||||||
if (isEditMode) return;
|
|
||||||
if (downloaded) {
|
|
||||||
await sendThemeUpdate(theme as DownloadedTheme, true)
|
|
||||||
DeleteDownloadedTheme(theme.id);
|
|
||||||
setTheme(theme.id);
|
|
||||||
} else {
|
|
||||||
onThemeSelect(theme.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onThemeDelete(theme.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShareClick = (event: React.MouseEvent) => {
|
|
||||||
event?.preventDefault();
|
|
||||||
setUploading(true);
|
|
||||||
browser.runtime.sendMessage({ type: 'currentTab', info: 'ShareTheme', body: { themeID: theme.id } }).then(() => {
|
|
||||||
setUploading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={`relative group w-full aspect-theme flex justify-center items-center rounded-xl transition ring dark:ring-white ring-zinc-300 ${
|
|
||||||
isSelected ? 'dark:ring-2 ring-4' : 'ring-0'
|
|
||||||
}`}
|
|
||||||
onClick={handleThemeClick}
|
|
||||||
>
|
|
||||||
{isEditMode && (
|
|
||||||
<div
|
|
||||||
className="absolute z-20 flex w-6 h-6 p-2 text-white transition-all rounded-full opacity-0 top-1 right-2 dark:bg-red-600 place-items-center group-hover:opacity-100 group-hover:top-2"
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
>
|
|
||||||
<div className="w-4 h-0.5 bg-white"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ ( !isEditMode ) && !downloaded /* && !theme.webURL */ ? (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="absolute z-20 flex w-8 h-8 p-2 text-white transition-all rounded-full delay-[20ms] opacity-0 top-1 right-2 bg-black/50 place-items-center group-hover:opacity-100 group-hover:top-[1.25rem]"
|
|
||||||
onClick={(event) => { event?.preventDefault(), browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenThemeCreator', body: { themeID: theme.id } }) }}
|
|
||||||
>
|
|
||||||
<PencilIcon className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="absolute z-20 flex w-8 h-8 p-2 text-white transition-all rounded-full opacity-0 top-1 right-12 bg-black/50 place-items-center group-hover:opacity-100 group-hover:top-[1.25rem]"
|
|
||||||
onClick={handleShareClick}
|
|
||||||
>
|
|
||||||
{uploading ? <LoadingSpinner size={16} /> : <ArrowUpOnSquareIcon className="w-4 h-4" />}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="relative top-0 z-10 flex justify-center w-full h-full overflow-hidden transition dark:text-white rounded-xl group place-items-center bg-zinc-100 dark:bg-zinc-900">
|
|
||||||
{theme.coverImage &&
|
|
||||||
<img
|
|
||||||
src={(typeof theme.coverImage) == 'string' ? theme.coverImage as string : URL.createObjectURL(theme.coverImage as Blob)}
|
|
||||||
alt={theme.name}
|
|
||||||
className="absolute inset-0 z-0 object-cover w-full h-full pointer-events-none"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
theme.hideThemeName ? <></> :
|
|
||||||
<div className={`z-10 ${theme.coverImage && 'text-white'}`}>{theme.name}</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const LoadingSpinner = ({ size }: { size: number }) => {
|
|
||||||
return <div style={{ width: `${size}px`, height: `${size}px` }} className={`animate-spin rounded-full border-2 border-white border-t-2 border-t-transparent`}></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import React, { forwardRef, ForwardRefExoticComponent, RefAttributes, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
|
||||||
import { deleteTheme, disableTheme, getDownloadedThemes, listThemes, sendThemeUpdate, setTheme } from '../hooks/ThemeManagment';
|
|
||||||
import { DeleteDownloadedTheme } from '../pages/Store';
|
|
||||||
import { ThemeCover } from './ThemeCover';
|
|
||||||
import browser from 'webextension-polyfill';
|
|
||||||
import { CustomTheme, DownloadedTheme } from '../types/CustomThemes';
|
|
||||||
import { useSettingsContext } from '../SettingsContext';
|
|
||||||
import { SettingsState } from '../types/AppProps';
|
|
||||||
import { InstallTheme } from '../../seqta/ui/themes/downloadTheme';
|
|
||||||
import SpinnerIcon from './LoadingSpinner';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
|
||||||
import useVisibility from './useVisibility';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { Mutex } from '../../seqta/utils/mutex';
|
|
||||||
|
|
||||||
interface ThemeSelectorProps {
|
|
||||||
isEditMode: boolean;
|
|
||||||
ref: React.Ref<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemeSelector: ForwardRefExoticComponent<Omit<ThemeSelectorProps, "ref"> & RefAttributes<any>> = forwardRef(({ isEditMode = false }, ref) => {
|
|
||||||
const [themes, setThemes] = useState<Omit<CustomTheme, 'CustomImages'>[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
|
||||||
const [tempTheme, setTempTheme] = useState<any>(null);
|
|
||||||
const { settingsState, setSettingsState } = useSettingsContext();
|
|
||||||
const [elementRef, isVisible] = useVisibility({
|
|
||||||
root: null, // Use the viewport as the root
|
|
||||||
rootMargin: '0px',
|
|
||||||
threshold: 0.1, // 10% of the element needs to be visible
|
|
||||||
});
|
|
||||||
|
|
||||||
const mutex = new Mutex();
|
|
||||||
|
|
||||||
const setSelectedTheme = (themeId: string) => {
|
|
||||||
setSettingsState((prevState: SettingsState) => ({
|
|
||||||
...prevState,
|
|
||||||
selectedTheme: themeId,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
disableTheme: async () => {
|
|
||||||
await disableTheme();
|
|
||||||
setSelectedTheme('');
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleThemeChange = async () => {
|
|
||||||
//await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
fetchThemes();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('message', (message) => {
|
|
||||||
if (message.data.type === 'themeChanged') {
|
|
||||||
handleThemeChange();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('message', (message) => {
|
|
||||||
if (message.data.type === 'themeChanged') {
|
|
||||||
handleThemeChange();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let intervalId: any;
|
|
||||||
if (isVisible) {
|
|
||||||
intervalId = setInterval(fetchThemes, 2000);
|
|
||||||
} else {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
};
|
|
||||||
}, [isVisible]);
|
|
||||||
|
|
||||||
const fetchThemes = async () => {
|
|
||||||
try {
|
|
||||||
const { themes, selectedTheme } = await listThemes();
|
|
||||||
let tempDownloadedThemes = await getDownloadedThemes();
|
|
||||||
|
|
||||||
setThemes(themes);
|
|
||||||
setSelectedTheme(selectedTheme ? selectedTheme : '');
|
|
||||||
|
|
||||||
const matchingThemes = themes.filter(theme =>
|
|
||||||
tempDownloadedThemes.some(downloadedTheme => downloadedTheme.id === theme.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingThemes.length > 0) {
|
|
||||||
matchingThemes.forEach((theme) => {
|
|
||||||
DeleteDownloadedTheme(theme.id);
|
|
||||||
tempDownloadedThemes = tempDownloadedThemes.filter(downloadedTheme => downloadedTheme.id !== theme.id);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
tempDownloadedThemes.forEach(async (theme) => {
|
|
||||||
await sendThemeUpdate(theme as DownloadedTheme, true, false)
|
|
||||||
DeleteDownloadedTheme(theme.id);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching themes:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchThemes();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleThemeSelect = useCallback(
|
|
||||||
async (themeId: string) => {
|
|
||||||
const unlock = await mutex.lock();
|
|
||||||
try {
|
|
||||||
if (themeId === settingsState.selectedTheme) {
|
|
||||||
await disableTheme();
|
|
||||||
setSelectedTheme('');
|
|
||||||
} else {
|
|
||||||
const selectedTheme = themes.find((theme) => theme.id === themeId);
|
|
||||||
if (selectedTheme) {
|
|
||||||
await setTheme(selectedTheme.id);
|
|
||||||
setSelectedTheme(themeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
unlock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[settingsState.selectedTheme, themes]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleThemeSelectDebounced = useCallback(
|
|
||||||
debounce(handleThemeSelect, 100),
|
|
||||||
[handleThemeSelect]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleThemeDelete = useCallback(
|
|
||||||
async (themeId: string) => {
|
|
||||||
try {
|
|
||||||
await deleteTheme(themeId);
|
|
||||||
setThemes((prevThemes) => prevThemes.filter((theme) => theme.id !== themeId));
|
|
||||||
if (themeId === settingsState.selectedTheme) {
|
|
||||||
setSelectedTheme('')
|
|
||||||
disableTheme();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting theme:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[settingsState.selectedTheme]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragLeave = () => {
|
|
||||||
setIsDragging(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(false);
|
|
||||||
const file: File = e.dataTransfer.files[0];
|
|
||||||
const reader: FileReader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = async (event: ProgressEvent<FileReader>) => {
|
|
||||||
try {
|
|
||||||
const result: any = JSON.parse(event.target!.result as string);
|
|
||||||
try {
|
|
||||||
setTempTheme(result);
|
|
||||||
await InstallTheme(result);
|
|
||||||
await fetchThemes();
|
|
||||||
setTempTheme(null);
|
|
||||||
} catch(error) {
|
|
||||||
toast.error('Invalid file type. Please upload a valid theme file.');
|
|
||||||
setTempTheme(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Error parsing file. Please upload a valid JSON theme file.');
|
|
||||||
setTempTheme(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div className='text-center'>Loading themes...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={elementRef}
|
|
||||||
className={`my-3 w-full`}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<div className={`${isDragging ? 'opacity-100' : 'opacity-0'} transition pointer-events-none absolute w-full p-2 z-50`}>
|
|
||||||
<div className='sticky w-full h-64 bg-white shadow-xl dark:bg-zinc-900 top-5 dark:text-white rounded-xl outline-dashed outline-4 outline-zinc-200 dark:outline-zinc-700'>
|
|
||||||
<div className='flex items-center justify-center h-full'>
|
|
||||||
<div className='flex flex-col items-center justify-center'>
|
|
||||||
<svg height="48" width="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g fill="currentColor">
|
|
||||||
<path d="M44,31a1,1,0,0,0-1,1v8a3,3,0,0,1-3,3H8a3,3,0,0,1-3-3V32a1,1,0,0,0-2,0v8a5.006,5.006,0,0,0,5,5H40a5.006,5.006,0,0,0,5-5V32A1,1,0,0,0,44,31Z" fill="currentColor"/>
|
|
||||||
<path d="M23.2,33.6a1,1,0,0,0,1.6,0l9-12A1,1,0,0,0,33,20H26V5a2,2,0,0,0-4,0V20H15a1,1,0,0,0-.8,1.6Z" fill="currentColor"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span className='text-lg'>Import Theme</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="pb-2 text-lg font-bold">Themes</h2>
|
|
||||||
<div className="flex flex-col gap-2 px-1">
|
|
||||||
|
|
||||||
{themes.map((theme) => (
|
|
||||||
<ThemeCover
|
|
||||||
key={theme.id}
|
|
||||||
theme={theme}
|
|
||||||
isSelected={theme.id === settingsState.selectedTheme}
|
|
||||||
isEditMode={isEditMode}
|
|
||||||
onThemeSelect={handleThemeSelectDebounced}
|
|
||||||
onThemeDelete={handleThemeDelete}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{tempTheme && (
|
|
||||||
<div className="flex justify-center w-full bg-gray-200 rounded-xl dark:bg-zinc-700/50 place-items-center aspect-theme animate-pulse">
|
|
||||||
<SpinnerIcon className='opacity-50' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ themes.length > 0 && <div
|
|
||||||
id="divider"
|
|
||||||
className="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"
|
|
||||||
></div>}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => browser.tabs.create({ url: browser.runtime.getURL('src/interface/index.html#store')})}
|
|
||||||
className="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
|
||||||
>
|
|
||||||
<span className="text-xl font-IconFamily">{'\uecc5'}</span>
|
|
||||||
<span className="ml-2">Theme Store</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenThemeCreator' })}
|
|
||||||
className="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
|
||||||
>
|
|
||||||
<span className="text-xl font-IconFamily">{'\uec60'}</span>
|
|
||||||
<span className="ml-2">Create your own</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ThemeSelector;
|
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader';
|
||||||
|
import Spinner from '../Spinner.svelte';
|
||||||
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||||
|
import { Index } from 'flexsearch';
|
||||||
|
import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'
|
||||||
|
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||||
|
|
||||||
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
|
||||||
|
type Background = { id: string; category: string; type: string; lowResUrl: string; highResUrl: string; name: string; description: string; featured?: boolean };
|
||||||
|
let { searchTerm } = $props<{ searchTerm: string }>();
|
||||||
|
|
||||||
|
// Existing states
|
||||||
|
let backgrounds = $state<Background[]>([]);
|
||||||
|
let selectedCategory = $state<string>('All');
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let selectedBackground = $state<string | null>(null);
|
||||||
|
let isLoading = $state<boolean>(true);
|
||||||
|
let savedBackgrounds = $state<string[]>([]);
|
||||||
|
let installingBackgrounds = $state<Set<string>>(new Set());
|
||||||
|
let debugInfo = $state<string>('');
|
||||||
|
let searchIndex = $state<Index | null>(null);
|
||||||
|
|
||||||
|
// New state variables
|
||||||
|
let activeTab = $state<'all' | 'installed' | 'photos' | 'videos'>('all');
|
||||||
|
let sortBy = $state<'newest' | 'popular' | 'name'>('newest');
|
||||||
|
|
||||||
|
// Existing functions
|
||||||
|
const loadStore = async () => {
|
||||||
|
try {
|
||||||
|
debugInfo = 'Fetching backgrounds...';
|
||||||
|
const response = await fetch('https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/backgrounds.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
backgrounds = data.backgrounds;
|
||||||
|
|
||||||
|
// Initialize FlexSearch index
|
||||||
|
const index = new Index({
|
||||||
|
tokenize: "forward",
|
||||||
|
preset: "score"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add backgrounds to the index
|
||||||
|
backgrounds.forEach((bg, i) => {
|
||||||
|
index.add(i, bg.name + " " + bg.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchIndex = index;
|
||||||
|
debugInfo = `Loaded ${backgrounds.length} backgrounds`;
|
||||||
|
await loadSavedBackgrounds();
|
||||||
|
} catch (e) {
|
||||||
|
error = 'Failed to load background store';
|
||||||
|
debugInfo = `Error: ${e instanceof Error ? e.message : 'Unknown error'}`;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadSavedBackgrounds(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!isIndexedDBSupported()) {
|
||||||
|
throw new Error("Your browser doesn't support IndexedDB.");
|
||||||
|
}
|
||||||
|
await openDatabase();
|
||||||
|
const data = await readAllData();
|
||||||
|
savedBackgrounds = data.map(item => item.id);
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data on mount
|
||||||
|
loadStore();
|
||||||
|
|
||||||
|
// Derived states
|
||||||
|
let filteredBackgrounds = $derived((() => {
|
||||||
|
let filtered = backgrounds;
|
||||||
|
|
||||||
|
// Use FlexSearch if there's a search term
|
||||||
|
if (searchTerm.trim() && searchIndex) {
|
||||||
|
const results = searchIndex.search(searchTerm) as number[];
|
||||||
|
filtered = results.map(i => backgrounds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply category filtering
|
||||||
|
filtered = filtered.filter((bg: Background) => {
|
||||||
|
return selectedCategory === 'All'
|
||||||
|
? true
|
||||||
|
: selectedCategory === 'Featured'
|
||||||
|
? bg.featured
|
||||||
|
: bg.category === selectedCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
filtered.sort((a: Background, b: Background) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'name':
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
case 'newest':
|
||||||
|
return -1;
|
||||||
|
case 'popular':
|
||||||
|
return -1;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
})());
|
||||||
|
|
||||||
|
let categories = $derived([...new Set(backgrounds.map(bg => bg.category))]);
|
||||||
|
|
||||||
|
// Background management functions
|
||||||
|
async function saveBackgroundFromUrl(url: string, id: string, fileType: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!isIndexedDBSupported()) {
|
||||||
|
throw new Error("Your browser doesn't support IndexedDB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const hasSpace = await hasEnoughStorageSpace(blob.size);
|
||||||
|
|
||||||
|
if (!hasSpace) {
|
||||||
|
throw new Error("Not enough storage space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeData(id, fileType, blob);
|
||||||
|
savedBackgrounds = [...savedBackgrounds, id];
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBackground(fileId: string): Promise<void> {
|
||||||
|
installingBackgrounds = new Set(installingBackgrounds).add(fileId);
|
||||||
|
try {
|
||||||
|
await deleteData(fileId);
|
||||||
|
savedBackgrounds = savedBackgrounds.filter(id => id !== fileId);
|
||||||
|
|
||||||
|
if (selectedBackground === fileId) {
|
||||||
|
selectNoBackground();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? `Failed to delete background: ${e.message}` : 'Unknown error occurred';
|
||||||
|
} finally {
|
||||||
|
installingBackgrounds = new Set(installingBackgrounds);
|
||||||
|
installingBackgrounds.delete(fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installBackground(background: Background) {
|
||||||
|
installingBackgrounds = new Set(installingBackgrounds).add(background.id);
|
||||||
|
try {
|
||||||
|
await saveBackgroundFromUrl(background.highResUrl, background.id, background.type);
|
||||||
|
backgroundUpdates.triggerUpdate();
|
||||||
|
} finally {
|
||||||
|
installingBackgrounds = new Set(installingBackgrounds);
|
||||||
|
installingBackgrounds.delete(background.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleBackgroundInstallation(background: Background) {
|
||||||
|
if (savedBackgrounds.includes(background.id)) {
|
||||||
|
await deleteBackground(background.id);
|
||||||
|
} else {
|
||||||
|
await installBackground(background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNoBackground() {
|
||||||
|
selectedBackground = null;
|
||||||
|
themeManager.setTheme('');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-full">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="p-4 w-64 h-full border-r border-zinc-200 dark:border-zinc-700">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="mb-4 text-lg font-semibold">Categories</h2>
|
||||||
|
<nav class="space-y-2">
|
||||||
|
<button
|
||||||
|
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === 'All' ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
|
||||||
|
onclick={() => selectedCategory = 'All'}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === 'Featured' ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
|
||||||
|
onclick={() => selectedCategory = 'Featured'}
|
||||||
|
>
|
||||||
|
Featured
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="my-2 border-b border-zinc-200 dark:border-zinc-700"></div>
|
||||||
|
|
||||||
|
{#each categories as category}
|
||||||
|
<button
|
||||||
|
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === category ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
|
||||||
|
onclick={() => selectedCategory = category}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="overflow-auto flex-1">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="sticky top-0 z-10 p-4 border-b bg-[#F1F1F3] dark:bg-zinc-900 dark:border-zinc-700">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h1 class="text-2xl font-bold">Explore Backgrounds {searchTerm ? `- "${searchTerm}"` : ''}</h1>
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<select
|
||||||
|
bind:value={sortBy}
|
||||||
|
class="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<option value="newest">Newest</option>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#each ['All', 'Installed', 'Photos', 'Videos'] as tab}
|
||||||
|
<button
|
||||||
|
class={`px-4 py-2 text-sm font-medium transition-colors rounded-full
|
||||||
|
${activeTab === tab.toLowerCase() ? 'bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700' :
|
||||||
|
'bg-zinc-100 dark:bg-transparent dark:outline dark:outline-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700/20'}`}
|
||||||
|
onclick={() => activeTab = tab.toLowerCase() as typeof activeTab}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Background Grid -->
|
||||||
|
<div class="p-4">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each Array(9) as _}
|
||||||
|
<div class="overflow-hidden relative rounded-lg animate-pulse">
|
||||||
|
<!-- Image placeholder -->
|
||||||
|
<div class="w-full h-48 bg-zinc-200 dark:bg-zinc-800"></div>
|
||||||
|
<!-- Gradient overlay -->
|
||||||
|
<div class="absolute right-0 bottom-0 left-0 h-16 to-transparent bg-linear-to-t from-zinc-300 dark:from-zinc-700">
|
||||||
|
<!-- Title placeholder -->
|
||||||
|
<div class="absolute right-2 bottom-2 left-2">
|
||||||
|
<div class="w-2/3 h-4 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>
|
||||||
|
<div class="mt-2 w-1/2 h-3 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="p-4 text-red-500 bg-red-100 rounded-lg">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each filteredBackgrounds.filter((bg: Background) => {
|
||||||
|
if (activeTab === 'installed') return savedBackgrounds.includes(bg.id);
|
||||||
|
if (activeTab === 'photos') return bg.type === 'image';
|
||||||
|
if (activeTab === 'videos') return bg.type !== 'image';
|
||||||
|
return true;
|
||||||
|
}) as background (background.id)}
|
||||||
|
<div
|
||||||
|
class="overflow-hidden relative rounded-lg shadow-lg cursor-pointer group"
|
||||||
|
onclick={() => toggleBackgroundInstallation(background)}
|
||||||
|
onkeydown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
toggleBackgroundInstallation(background);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
{#if background.type === 'image'}
|
||||||
|
<img src={background.lowResUrl} alt={background.name} class="object-cover w-full h-48 transition-all duration-300 group-hover:scale-105" />
|
||||||
|
{:else}
|
||||||
|
<video src={background.lowResUrl} class="object-cover w-full h-48" muted loop autoplay></video>
|
||||||
|
{/if}
|
||||||
|
<div class={`flex absolute inset-0 justify-center items-center opacity-0 transition-opacity duration-300 bg-black/50 group-hover:opacity-100 ${installingBackgrounds.has(background.id) ? 'opacity-100' : ''}`}>
|
||||||
|
{#if installingBackgrounds.has(background.id)}
|
||||||
|
<Spinner />
|
||||||
|
{:else if savedBackgrounds.includes(background.id)}
|
||||||
|
<span class="flex items-center text-white">
|
||||||
|
<span class="mr-2 text-2xl not-italic font-IconFamily" aria-hidden="true"></span>
|
||||||
|
<span class="text-sm font-semibold">Remove</span>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="flex items-center text-white">
|
||||||
|
<span class="mr-2 text-2xl not-italic font-IconFamily" aria-hidden="true"></span>
|
||||||
|
<span class="text-sm font-semibold">Install</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if settingsState.devMode}
|
||||||
|
<div class="p-4 mt-8 rounded bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
<h3 class="mb-2 font-bold">Debug Info:</h3>
|
||||||
|
<p>{debugInfo}</p>
|
||||||
|
<p>Total backgrounds: {backgrounds.length}</p>
|
||||||
|
<p>Categories: {categories.join(', ') || '<empty>'}</p>
|
||||||
|
<p>Active Tab: {activeTab}</p>
|
||||||
|
<p>Selected Category: {selectedCategory}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import type { Theme } from '@/interface/types/Theme';
|
||||||
|
import emblaCarouselSvelte from 'embla-carousel-svelte';
|
||||||
|
import Autoplay from 'embla-carousel-autoplay';
|
||||||
|
|
||||||
|
let { coverThemes, setDisplayTheme } = $props<{ coverThemes: Theme[], setDisplayTheme: (theme: Theme) => void }>();
|
||||||
|
let emblaApi = $state();
|
||||||
|
|
||||||
|
const options = { loop: true };
|
||||||
|
const plugins = [
|
||||||
|
Autoplay({
|
||||||
|
delay: 5000,
|
||||||
|
stopOnInteraction: false,
|
||||||
|
stopOnMouseEnter: true
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
function onInit(event: CustomEvent) {
|
||||||
|
emblaApi = event.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const slidePrev = () => emblaApi?.scrollPrev();
|
||||||
|
// @ts-ignore
|
||||||
|
const slideNext = () => emblaApi?.scrollNext();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if coverThemes.length > 0}
|
||||||
|
<div class="relative w-full overflow-clip rounded-xl transition-opacity" transition:fade>
|
||||||
|
<div
|
||||||
|
class="w-full aspect-8/3"
|
||||||
|
use:emblaCarouselSvelte={{ options, plugins }}
|
||||||
|
onemblaInit={onInit}
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
{#each coverThemes as theme}
|
||||||
|
<div
|
||||||
|
class="relative flex-[0_0_100%] cursor-pointer rounded-xl overflow-clip"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) => { if (e.key === 'Enter') setDisplayTheme(theme) }}
|
||||||
|
onclick={() => setDisplayTheme(theme)}
|
||||||
|
>
|
||||||
|
<img src={theme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-full" />
|
||||||
|
<div class='absolute bottom-0 left-0 p-8 z-[1]'>
|
||||||
|
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
|
||||||
|
<p class='text-lg text-white'>{theme.description}</p>
|
||||||
|
</div>
|
||||||
|
<div class='absolute bottom-0 left-0 w-full h-1/2 to-transparent bg-linear-to-t from-black/80'></div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation buttons -->
|
||||||
|
<div class='flex absolute right-2 bottom-2 z-10 gap-2'>
|
||||||
|
<button aria-label="Previous" onclick={slidePrev} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button aria-label="Next" onclick={slideNext} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let filters = $state({
|
||||||
|
type: [] as string[],
|
||||||
|
color: [] as string[],
|
||||||
|
resolution: [] as string[],
|
||||||
|
orientation: [] as string[]
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
dispatch('filter', filters);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleFilter(category: keyof typeof filters, value: string) {
|
||||||
|
if (filters[category].includes(value)) {
|
||||||
|
filters[category] = filters[category].filter(v => v !== value);
|
||||||
|
} else {
|
||||||
|
filters[category] = [...filters[category], value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
filters = {
|
||||||
|
type: [],
|
||||||
|
color: [],
|
||||||
|
resolution: [],
|
||||||
|
orientation: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold">Filters</h2>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="mb-2 font-medium">Type</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" checked={filters.type.includes('image')} onchange={() => toggleFilter('type', 'image')}>
|
||||||
|
<span class="ml-2">Image</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" checked={filters.type.includes('video')} onchange={() => toggleFilter('type', 'video')}>
|
||||||
|
<span class="ml-2">Video</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 mt-4 text-white bg-red-500 rounded hover:bg-red-600"
|
||||||
|
onclick={clearFilters}
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import logo from '@/resources/icons/betterseqta-dark-full.png';
|
||||||
|
import logoDark from '@/resources/icons/betterseqta-light-full.png';
|
||||||
|
import { closeStore } from '@/seqta/ui/renderStore'
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
let { searchTerm, setSearchTerm, darkMode, activeTab, setActiveTab } = $props<{
|
||||||
|
searchTerm: string,
|
||||||
|
setSearchTerm: (term: string) => void,
|
||||||
|
darkMode: boolean,
|
||||||
|
activeTab: string,
|
||||||
|
setActiveTab: (tab: string) => void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Clear search input function
|
||||||
|
const clearSearch = () => {
|
||||||
|
setSearchTerm('');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="fixed top-0 z-50 w-full h-[4.25rem] bg-white border-b shadow-md border-b-white/10 dark:bg-zinc-950/90 backdrop-blur-xl dark:text-white">
|
||||||
|
<div class="flex justify-between items-center px-4 py-1">
|
||||||
|
<div class="flex gap-4 place-items-center cursor-pointer" onkeydown={(e) => { if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0">
|
||||||
|
<img src={browser.runtime.getURL(logo)} class="h-14 {darkMode ? 'hidden' : ''}" alt="Logo" />
|
||||||
|
<img src={browser.runtime.getURL(logoDark)} class="h-14 {darkMode ? '' : 'hidden'}" alt="Dark Logo" />
|
||||||
|
|
||||||
|
<div class="w-[1px] h-10 my-auto bg-zinc-400 dark:bg-zinc-600"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 font-semibold text-lg transition-colors duration-200 {activeTab === 'themes' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'}"
|
||||||
|
onclick={() => setActiveTab('themes')}
|
||||||
|
>
|
||||||
|
Themes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 font-semibold text-lg transition-colors duration-200 {activeTab === 'backgrounds' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'}"
|
||||||
|
onclick={() => setActiveTab('backgrounds')}
|
||||||
|
>
|
||||||
|
Backgrounds
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex relative gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search themes..."
|
||||||
|
value={searchTerm}
|
||||||
|
oninput={(e: any) => setSearchTerm(e.target.value)}
|
||||||
|
class="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-none focus:border-transparent" />
|
||||||
|
<svg
|
||||||
|
class="absolute left-3 top-1/2 w-5 h-5 text-gray-400 transform -translate-y-1/2 dark:text-gray-200"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Close Button -->
|
||||||
|
<button
|
||||||
|
onclick={closeStore}
|
||||||
|
class="p-1 px-3"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-IconFamily"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Theme } from '@/interface/types/Theme'
|
||||||
|
|
||||||
|
let { theme, onClick } = $props<{ theme: Theme; onClick: () => void }>();
|
||||||
|
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={onClick}>
|
||||||
|
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border" transition:fade>
|
||||||
|
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white">
|
||||||
|
{theme.name}
|
||||||
|
</div>
|
||||||
|
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></div>
|
||||||
|
<div class='w-full'>
|
||||||
|
<img src={theme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Theme } from '@/interface/types/Theme'
|
||||||
|
import ThemeCard from './ThemeCard.svelte';
|
||||||
|
|
||||||
|
let { themes, searchTerm, setDisplayTheme } = $props<{ themes: Theme[]; searchTerm: string, setDisplayTheme: (theme: Theme) => void }>();
|
||||||
|
|
||||||
|
let filteredThemes = $derived(themes.filter((theme: Theme) =>
|
||||||
|
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) || theme.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative" >
|
||||||
|
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each filteredThemes as theme (theme.id)}
|
||||||
|
<ThemeCard theme={theme} onClick={() => setDisplayTheme(theme)} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if filteredThemes.length !== 0}
|
||||||
|
<a href="https://betterseqta.gitbook.io/betterseqta-docs" class='w-full cursor-pointer'>
|
||||||
|
<div class="bg-zinc-50 h-48 w-full transition-all hover:scale-105 duration-500 relative justify-center items-center group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] rounded-xl overflow-clip border">
|
||||||
|
<div class="text-2xl font-IconFamily">{'\uecb3'}</div>
|
||||||
|
<div class="text-xl font-bold text-center transition-all duration-500 dark:text-white">
|
||||||
|
Got a Theme Idea?
|
||||||
|
<p class="text-lg font-light subtitle">Transform it into a stunning theme!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{#if filteredThemes.length === 0}
|
||||||
|
<div class="absolute top-0 flex flex-col items-center justify-center w-full text-center h-96">
|
||||||
|
<h1 class="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">That doesn't exist! 😭😭😭</h1>
|
||||||
|
<p class="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, we couldn't find the theme you're looking for. Maybe... you could create it?</p>
|
||||||
|
<a href="https://betterseqta.gitbook.io/betterseqta-docs" class='p-2 px-3 mt-4 transition rounded-md cursor-pointer dark:text-white bg-zinc-500/10 hover:scale-105'>
|
||||||
|
Show me how!
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Theme } from '@/interface/types/Theme'
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { animate } from 'motion';
|
||||||
|
|
||||||
|
let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme } = $props<{
|
||||||
|
theme: Theme | null;
|
||||||
|
currentThemes: string[];
|
||||||
|
setDisplayTheme: (theme: Theme | null) => void;
|
||||||
|
onInstall: (themeId: string) => void;
|
||||||
|
onRemove: (themeId: string) => void;
|
||||||
|
allThemes: Theme[];
|
||||||
|
displayTheme: Theme | null;
|
||||||
|
}>();
|
||||||
|
let installing = $state(false);
|
||||||
|
let modalElement: HTMLElement;
|
||||||
|
|
||||||
|
// Function to get related themes
|
||||||
|
function getRelatedThemes() {
|
||||||
|
return allThemes
|
||||||
|
.filter((t: Theme) => t.id !== theme.id)
|
||||||
|
.sort((a: Theme, b: Theme) => a.name.localeCompare(theme.name) - b.name.localeCompare(theme.name))
|
||||||
|
.slice(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (displayTheme) {
|
||||||
|
animate(
|
||||||
|
modalElement,
|
||||||
|
{ y: [500, 0], opacity: [0, 1] },
|
||||||
|
{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 150,
|
||||||
|
damping: 20
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hideModal = (relatedTheme?: Theme | null) => {
|
||||||
|
animate(
|
||||||
|
modalElement,
|
||||||
|
{ y: [10, 500], opacity: [1, 0] },
|
||||||
|
{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 150,
|
||||||
|
damping: 20
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisplayTheme(relatedTheme ?? null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex fixed inset-0 z-50 justify-center items-end bg-black/70"
|
||||||
|
onclick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) hideModal();
|
||||||
|
}}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) hideModal();
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
transition:fade
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={modalElement}
|
||||||
|
class="w-full max-w-[600px] h-[95%] p-4 bg-white rounded-t-2xl dark:bg-zinc-800 overflow-scroll no-scrollbar cursor-auto"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div class="relative h-auto">
|
||||||
|
<button class="absolute top-0 right-0 p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200" onclick={() => hideModal()}>
|
||||||
|
{'\ued8a'}
|
||||||
|
</button>
|
||||||
|
<h2 class="mb-4 text-2xl font-bold">
|
||||||
|
{theme.name}
|
||||||
|
</h2>
|
||||||
|
<img src={theme.marqueeImage} alt="Theme Cover" class="object-cover mb-4 w-full rounded-md" />
|
||||||
|
<p class="mb-4 text-gray-700 dark:text-gray-300">
|
||||||
|
{theme.description}
|
||||||
|
</p>
|
||||||
|
{#if currentThemes.includes(theme.id)}
|
||||||
|
<button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||||
|
{#if installing}
|
||||||
|
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||||
|
{#if installing}
|
||||||
|
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Install</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="my-8 border-b border-zinc-200 dark:border-zinc-700"></div>
|
||||||
|
|
||||||
|
<h3 class="mb-4 text-lg font-bold">
|
||||||
|
Similar Themes
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
{#each getRelatedThemes() as relatedTheme (relatedTheme.id)}
|
||||||
|
<button onclick={() => { hideModal(relatedTheme) }} class="w-full cursor-pointer">
|
||||||
|
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
|
||||||
|
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5">
|
||||||
|
{relatedTheme.name}
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 z-0 w-full h-3/4 to-transparent from-black/80 bg-linear-to-t"></div>
|
||||||
|
<img src={relatedTheme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-48" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import logo from '../../../resources/icons/betterseqta-dark-full.png';
|
|
||||||
import logoDark from '../../../resources/icons/betterseqta-light-full.png';
|
|
||||||
|
|
||||||
export default function header({ searchTerm, setSearchTerm }: { searchTerm: string, setSearchTerm: (value: string) => void }) {
|
|
||||||
return <header className="fixed top-0 z-50 w-full h-[4.25rem] bg-white border-b shadow-md border-b-white/10 dark:bg-zinc-800/90 backdrop-blur-xl">
|
|
||||||
<div className="flex items-center justify-between px-4 py-1">
|
|
||||||
<div className="flex gap-4 cursor-pointer place-items-center" onClick={() => setSearchTerm('')}>
|
|
||||||
<img src={logo} className="h-14 dark:hidden" />
|
|
||||||
<img src={logoDark} className="hidden h-14 dark:block" />
|
|
||||||
|
|
||||||
<div className="w-[1px] h-10 my-auto bg-zinc-400 dark:bg-zinc-600" />
|
|
||||||
|
|
||||||
<h1 className="text-xl font-semibold">Theme Store</h1>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search themes..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-none focus:border-transparent" />
|
|
||||||
<svg
|
|
||||||
className="absolute w-5 h-5 text-gray-400 transform -translate-y-1/2 left-3 top-1/2 dark:text-gray-200"
|
|
||||||
fill="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<script lang="ts"></script>
|
||||||
|
|
||||||
|
<div class='w-full h-0.5 my-4 bg-zinc-200 dark:bg-zinc-700'></div>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Background {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
blob: Blob | null;
|
||||||
|
url?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { bg, isSelected, isEditMode, onClick, onDelete } = $props<{ bg: Background, isSelected: boolean, isEditMode: boolean, onClick: () => void, onDelete: () => void }>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onclick={onClick}
|
||||||
|
onkeydown={onClick}
|
||||||
|
tabindex="-1"
|
||||||
|
role="button"
|
||||||
|
class="relative w-16 h-16 cursor-pointer rounded-xl transition ring-3 dark:ring-zinc-500/50 ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-4 ring-4' : 'ring-0'}"
|
||||||
|
>
|
||||||
|
{#if isEditMode}
|
||||||
|
<div
|
||||||
|
tabindex="-1"
|
||||||
|
role="button"
|
||||||
|
class="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||||
|
onclick={onDelete}
|
||||||
|
onkeydown={onDelete}
|
||||||
|
>
|
||||||
|
<div class="w-4 h-0.5 bg-white"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if bg.url}
|
||||||
|
{#if bg.type === 'image'}
|
||||||
|
<img class="object-cover w-full h-full rounded-xl" src={bg.url} alt="swatch" />
|
||||||
|
{:else if bg.type === 'video'}
|
||||||
|
<video muted loop autoplay src={bg.url} class="object-cover w-full h-full rounded-xl"></video>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader'
|
||||||
|
import BackgroundUploader from './BackgroundUploader.svelte';
|
||||||
|
import BackgroundItem from './BackgroundItem.svelte'
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
|
||||||
|
import { delay } from 'lodash'
|
||||||
|
import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'
|
||||||
|
|
||||||
|
let { isEditMode, selectNoBackground = $bindable(), selectedBackground = $bindable() } = $props<{ isEditMode: boolean, selectNoBackground: () => void, selectedBackground: string | null }>();
|
||||||
|
let backgrounds = $state<{ id: string; type: string; blob: Blob | null; url?: string }[]>([]);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
|
let imageBackgrounds = $derived(backgrounds.filter(bg => bg.type === 'image'));
|
||||||
|
let videoBackgrounds = $derived(backgrounds.filter(bg => bg.type === 'video'));
|
||||||
|
|
||||||
|
let isVisible = $state(false);
|
||||||
|
let element: HTMLElement;
|
||||||
|
let observer: MutationObserver;
|
||||||
|
let parentElement: HTMLElement | null = null;
|
||||||
|
|
||||||
|
async function getTheme() {
|
||||||
|
return localStorage.getItem('selectedBackground');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setTheme(theme: string) {
|
||||||
|
localStorage.setItem('selectedBackground', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileChange(file: File): Promise<void> {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isIndexedDBSupported()) {
|
||||||
|
throw new Error("Your browser doesn't support IndexedDB. Unable to save backgrounds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSpace = await hasEnoughStorageSpace(file.size);
|
||||||
|
if (!hasSpace) {
|
||||||
|
throw new Error("Not enough storage space to save this background.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = `${Date.now()}-${file.name}`;
|
||||||
|
const fileType = file.type.split('/')[0];
|
||||||
|
const blob = new Blob([file], { type: file.type });
|
||||||
|
|
||||||
|
await writeData(fileId, fileType, blob);
|
||||||
|
backgrounds = [...backgrounds, { id: fileId, type: fileType, blob, url: URL.createObjectURL(blob) }];
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
error = e.message;
|
||||||
|
} else {
|
||||||
|
error = 'An unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBackgroundMetadata(): Promise<void> {
|
||||||
|
try {
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
if (!isIndexedDBSupported()) {
|
||||||
|
throw new Error("Your browser doesn't support IndexedDB. Unable to load backgrounds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await openDatabase();
|
||||||
|
const data = await readAllData();
|
||||||
|
selectedBackground = await getTheme();
|
||||||
|
|
||||||
|
// Only load metadata (id and type) for placeholders
|
||||||
|
backgrounds = data.map(({ id, type }) => ({ id, type, blob: null }));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
error = e.message;
|
||||||
|
} else {
|
||||||
|
error = 'An unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncBackgrounds(): Promise<void> {
|
||||||
|
try {
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
if (!isIndexedDBSupported()) {
|
||||||
|
throw new Error("Your browser doesn't support IndexedDB. Unable to load backgrounds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbData = await readAllData();
|
||||||
|
|
||||||
|
// Release existing object URLs to prevent memory leaks
|
||||||
|
backgrounds.forEach(bg => {
|
||||||
|
if (bg.url) URL.revokeObjectURL(bg.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create fresh background objects with new object URLs
|
||||||
|
backgrounds = dbData.map(bg => ({
|
||||||
|
id: bg.id,
|
||||||
|
type: bg.type,
|
||||||
|
blob: bg.blob,
|
||||||
|
url: URL.createObjectURL(bg.blob)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Check if selected background still exists
|
||||||
|
if (selectedBackground && !backgrounds.some(bg => bg.id === selectedBackground)) {
|
||||||
|
selectNoBackground();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
error = e.message;
|
||||||
|
} else {
|
||||||
|
error = 'An unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBackground(fileId: string): void {
|
||||||
|
if (selectedBackground === fileId) {
|
||||||
|
selectNoBackground();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedBackground = fileId;
|
||||||
|
setTheme(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBackground(fileId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await deleteData(fileId);
|
||||||
|
backgrounds = backgrounds.filter(bg => bg.id !== fileId);
|
||||||
|
|
||||||
|
if (selectedBackground === fileId) {
|
||||||
|
selectNoBackground();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
error = `Failed to delete background: ${e.message}`;
|
||||||
|
} else {
|
||||||
|
error = 'An unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNoBackground = () => {
|
||||||
|
selectedBackground = null;
|
||||||
|
setTheme('');
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
loadBackground();
|
||||||
|
selectedBackground
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkActiveClass() {
|
||||||
|
if (parentElement?.classList.contains('active')) {
|
||||||
|
delay(() => {
|
||||||
|
isVisible = true;
|
||||||
|
syncBackgrounds();
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadBackgroundMetadata();
|
||||||
|
backgroundUpdates.addListener(syncBackgrounds);
|
||||||
|
|
||||||
|
parentElement = element.closest('.tab');
|
||||||
|
if (parentElement) {
|
||||||
|
observer = new MutationObserver(checkActiveClass);
|
||||||
|
observer.observe(parentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
backgroundUpdates.removeListener(syncBackgrounds);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={element} class="relative px-1 { !( isEditMode && imageBackgrounds.length === 0 && videoBackgrounds.length === 0 ) && 'pt-2' }">
|
||||||
|
{#if !(imageBackgrounds.length === 0 && isEditMode)}
|
||||||
|
<h2 class="pb-2 text-lg font-bold">Background Images</h2>
|
||||||
|
<div class="flex flex-wrap gap-4 mb-4">
|
||||||
|
{#if !isEditMode}
|
||||||
|
<BackgroundUploader on:fileChange={e => handleFileChange(e.detail)} />
|
||||||
|
{/if}
|
||||||
|
{#each imageBackgrounds as bg (bg.id)}
|
||||||
|
{#if isVisible && bg.blob}
|
||||||
|
<BackgroundItem
|
||||||
|
bg={bg}
|
||||||
|
isSelected={selectedBackground === bg.id}
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
onClick={() => selectBackground(bg.id)}
|
||||||
|
onDelete={() => deleteBackground(bg.id)}/>
|
||||||
|
{:else}
|
||||||
|
<div class="w-16 h-16 rounded-xl bg-zinc-100 dark:bg-zinc-900 animate-pulse"></div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !(videoBackgrounds.length === 0 && isEditMode)}
|
||||||
|
<h2 class="py-2 text-lg font-bold">Background Videos</h2>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
{#if !isEditMode}
|
||||||
|
<BackgroundUploader on:fileChange={e => handleFileChange(e.detail)} />
|
||||||
|
{/if}
|
||||||
|
{#each videoBackgrounds as bg (bg.id)}
|
||||||
|
{#if isVisible && bg.blob}
|
||||||
|
<BackgroundItem
|
||||||
|
bg={bg}
|
||||||
|
isSelected={selectedBackground === bg.id}
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
onClick={() => selectBackground(bg.id)}
|
||||||
|
onDelete={() => deleteBackground(bg.id)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="w-16 h-16 rounded-xl bg-zinc-100 dark:bg-zinc-900 animate-pulse"></div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleFileChange(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
dispatch('fileChange', file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative w-16 h-16 overflow-hidden transition rounded-xl bg-zinc-100 dark:bg-zinc-900">
|
||||||
|
<div class="flex items-center justify-center w-full h-full text-3xl font-bold text-gray-400 transition font-IconFamily hover:text-gray-500">
|
||||||
|
<!-- Plus icon -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*, video/mp4"
|
||||||
|
on:change={handleFileChange}
|
||||||
|
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
||||||
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
import { OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
||||||
|
import { OpenStorePage } from '@/seqta/ui/renderStore'
|
||||||
|
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
|
||||||
|
import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup'
|
||||||
|
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||||
|
|
||||||
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
|
||||||
|
let themes = $state<ThemeList | null>(null);
|
||||||
|
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
||||||
|
let isDragging = $state(false);
|
||||||
|
let tempTheme = $state(null);
|
||||||
|
|
||||||
|
const handleThemeClick = async (theme: CustomTheme) => {
|
||||||
|
if (isEditMode) return;
|
||||||
|
if (theme.id === themes?.selectedTheme) {
|
||||||
|
await themeManager.disableTheme();
|
||||||
|
themes.selectedTheme = '';
|
||||||
|
} else {
|
||||||
|
await themeManager.setTheme(theme.id);
|
||||||
|
if (!themes) return;
|
||||||
|
themes.selectedTheme = theme.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleThemeDelete = async (themeId: string) => {
|
||||||
|
try {
|
||||||
|
await themeManager.deleteTheme(themeId);
|
||||||
|
if (!themes) return;
|
||||||
|
|
||||||
|
themes.themes = themes.themes.filter(theme => theme.id !== themeId);
|
||||||
|
if (themeId === themes.selectedTheme) {
|
||||||
|
themes.selectedTheme = '';
|
||||||
|
await themeManager.disableTheme();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting theme:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShareTheme = async (theme: CustomTheme) => {
|
||||||
|
try {
|
||||||
|
await themeManager.shareTheme(theme.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sharing theme:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = async (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging = false;
|
||||||
|
const file = e.dataTransfer?.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event: ProgressEvent<FileReader>) => {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(event.target?.result as string);
|
||||||
|
tempTheme = result;
|
||||||
|
await themeManager.installTheme(result);
|
||||||
|
await fetchThemes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing file:', error);
|
||||||
|
alert('Error parsing file. Please upload a valid JSON theme file.');
|
||||||
|
}
|
||||||
|
tempTheme = null;
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchThemes = async () => {
|
||||||
|
themes = {
|
||||||
|
themes: await themeManager.getAvailableThemes(),
|
||||||
|
selectedTheme: themeManager.getSelectedThemeId() || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await fetchThemes();
|
||||||
|
|
||||||
|
themeUpdates.addListener(fetchThemes);
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
themeUpdates.removeListener(fetchThemes);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="pt-5 mb-1 w-full"
|
||||||
|
role="list"
|
||||||
|
tabindex="-1"
|
||||||
|
ondragover={handleDragOver}
|
||||||
|
ondragleave={handleDragLeave}
|
||||||
|
ondrop={handleDrop}
|
||||||
|
>
|
||||||
|
<div class="{isDragging ? 'opacity-100' : 'opacity-0'} transition pointer-events-none absolute w-full p-2 z-50">
|
||||||
|
<div class="sticky top-5 w-full h-64 bg-white rounded-xl shadow-xl dark:bg-zinc-900 dark:text-white outline-dashed outline-4 outline-zinc-200 dark:outline-zinc-700">
|
||||||
|
<div class="flex justify-center items-center h-full">
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<svg height="48" width="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="currentColor">
|
||||||
|
<path d="M44,31a1,1,0,0,0-1,1v8a3,3,0,0,1-3,3H8a3,3,0,0,1-3-3V32a1,1,0,0,0-2,0v8a5.006,5.006,0,0,0,5,5H40a5.006,5.006,0,0,0,5-5V32A1,1,0,0,0,44,31Z" fill="currentColor"/>
|
||||||
|
<path d="M23.2,33.6a1,1,0,0,0,1.6,0l9-12A1,1,0,0,0,33,20H26V5a2,2,0,0,0-4,0V20H15a1,1,0,0,0-.8,1.6Z" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span class="text-lg">Import Theme</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="pb-2 text-lg font-bold">Themes</h2>
|
||||||
|
<div class="flex flex-col gap-2 px-2">
|
||||||
|
{#if themes}
|
||||||
|
{#each themes.themes as theme (theme.id)}
|
||||||
|
<button
|
||||||
|
class="relative group w-full aspect-theme flex justify-center items-center rounded-xl transition ring dark:ring-white ring-zinc-300 {theme.id === themes.selectedTheme ? 'dark:ring-2 ring-4' : 'ring-0'}"
|
||||||
|
onclick={() => handleThemeClick(theme)}
|
||||||
|
>
|
||||||
|
{#if isEditMode}
|
||||||
|
<div
|
||||||
|
class="flex absolute top-2 right-2 z-20 place-items-center p-2 w-6 h-6 text-white bg-red-600 rounded-full opacity-100"
|
||||||
|
onclick={(event) => { event.stopPropagation(); handleThemeDelete(theme.id) }}
|
||||||
|
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleThemeDelete(theme.id) }}
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="w-4 h-0.5 bg-white"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !isEditMode}
|
||||||
|
<div
|
||||||
|
class="absolute z-20 flex w-8 h-8 p-2 text-white transition-all rounded-full delay-[20ms] opacity-0 top-1/4 right-2 bg-black/50 place-items-center group-hover:opacity-100 group-hover:top-1/2 -translate-y-1/2"
|
||||||
|
onclick={(event) => { event.stopPropagation(); OpenThemeCreator(theme.id); closeExtensionPopup() }}
|
||||||
|
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') OpenThemeCreator(theme.id); closeExtensionPopup() }}
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-IconFamily"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex absolute right-12 top-1/4 z-20 place-items-center p-2 w-8 h-8 text-center rounded-full opacity-0 transition-all -translate-y-1/2 text-white/80 bg-black/50 group-hover:opacity-100 group-hover:top-1/2"
|
||||||
|
onclick={(event) => { event.stopPropagation(); handleShareTheme(theme) }}
|
||||||
|
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleShareTheme(theme) }}
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-IconFamily"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="relative top-0 z-10 flex justify-center w-full h-full overflow-hidden transition dark:text-white rounded-xl group place-items-center bg-zinc-100 dark:bg-zinc-900 { isEditMode ? 'animate-shake brightness-90' : ''}">
|
||||||
|
{#if theme.coverImage}
|
||||||
|
<img
|
||||||
|
src={typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}
|
||||||
|
alt={theme.name}
|
||||||
|
class="object-cover absolute inset-0 z-0 w-full h-full pointer-events-none"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if !theme.hideThemeName}
|
||||||
|
<div class="z-10 {theme.coverImage ? 'text-white' : ''}">{theme.name}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if tempTheme}
|
||||||
|
<div class="flex justify-center place-items-center w-full bg-gray-200 rounded-xl animate-pulse dark:bg-zinc-700/50 aspect-theme">
|
||||||
|
<svg class="w-5 h-5 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if themes && themes.themes.length > 0}
|
||||||
|
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => OpenStorePage()}
|
||||||
|
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<span class="text-xl font-IconFamily"></span>
|
||||||
|
<span class="ml-2">Theme Store</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => { OpenThemeCreator(); closeExtensionPopup() }}
|
||||||
|
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<span class="text-xl font-IconFamily"></span>
|
||||||
|
<span class="ml-2">Create your own</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
root?: Element | null;
|
|
||||||
rootMargin?: string;
|
|
||||||
threshold?: number | number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type UseVisibilityReturnType = [any | null, boolean];
|
|
||||||
|
|
||||||
const useVisibility = (options: Options): UseVisibilityReturnType => {
|
|
||||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
|
||||||
const elementRef = useRef<Element | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(([entry]) => {
|
|
||||||
setIsVisible(entry.isIntersecting);
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
if (elementRef.current) {
|
|
||||||
observer.observe(elementRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (elementRef.current) {
|
|
||||||
observer.unobserve(elementRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [elementRef, options]);
|
|
||||||
|
|
||||||
return [elementRef, isVisible];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useVisibility;
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
|
||||||
|
const e = React.createElement;
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const { el, children, class: _, ...props } = $$props;
|
||||||
|
try {
|
||||||
|
ReactDOM.render(e(el, props, children), container);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`react-adapter failed to mount.`, { err });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
try {
|
||||||
|
ReactDOM.unmountComponentAtNode(container);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`react-adapter failed to unmount.`, { err });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={container} class={$$props.class}></div>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import Browser from "webextension-polyfill";
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const result = await Browser.storage.local.get();
|
|
||||||
if (result.DarkMode) {
|
|
||||||
document.body.classList.add('dark');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { type DBSchema, type IDBPDatabase, openDB } from 'idb';
|
||||||
|
|
||||||
|
interface BackgroundDB extends DBSchema {
|
||||||
|
backgrounds: {
|
||||||
|
key: string;
|
||||||
|
value: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
blob: Blob;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let db: IDBPDatabase<BackgroundDB> | null = null;
|
||||||
|
|
||||||
|
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
|
||||||
|
if (db) return db;
|
||||||
|
|
||||||
|
db = await openDB<BackgroundDB>('BackgroundDB', 1, {
|
||||||
|
upgrade(db: IDBPDatabase<BackgroundDB>) {
|
||||||
|
db.createObjectStore('backgrounds', { keyPath: 'id' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAllData(): Promise<Array<{ id: string; type: string; blob: Blob }>> {
|
||||||
|
const db = await openDatabase();
|
||||||
|
return db.getAll('backgrounds');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeData(id: string, type: string, blob: Blob): Promise<void> {
|
||||||
|
const db = await openDatabase();
|
||||||
|
await db.put('backgrounds', { id, type, blob });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteData(id: string): Promise<void> {
|
||||||
|
const db = await openDatabase();
|
||||||
|
await db.delete('backgrounds', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearAllData(): Promise<void> {
|
||||||
|
const db = await openDatabase();
|
||||||
|
await db.clear('backgrounds');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDataById(id: string): Promise<{ id: string; type: string; blob: Blob } | undefined> {
|
||||||
|
const db = await openDatabase();
|
||||||
|
return db.get('backgrounds', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeDatabase(): void {
|
||||||
|
if (db) {
|
||||||
|
db.close();
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if IndexedDB is supported
|
||||||
|
export function isIndexedDBSupported(): boolean {
|
||||||
|
return 'indexedDB' in window;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if there's enough storage space
|
||||||
|
export async function hasEnoughStorageSpace(requiredSpace: number): Promise<boolean> {
|
||||||
|
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
||||||
|
const { quota, usage } = await navigator.storage.estimate();
|
||||||
|
if (quota !== undefined && usage !== undefined) {
|
||||||
|
return (quota - usage) > requiredSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we can't determine, assume there's enough space
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Background } from "../components/BackgroundSelector";
|
|
||||||
|
|
||||||
export const downloadPresetBackground = async (background: Background, onProgress: (progress: number) => void): Promise<Background> => {
|
|
||||||
const response = await fetch(background.url as string);
|
|
||||||
|
|
||||||
const totalLength = +response.headers.get('Content-Length')!;
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
const chunks = [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader!.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
chunks.push(value!);
|
|
||||||
receivedLength += value!.length;
|
|
||||||
|
|
||||||
onProgress(Math.ceil(receivedLength / totalLength * 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob(chunks);
|
|
||||||
await writeData(background.id, background.type, blob);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: background.id,
|
|
||||||
type: background.type,
|
|
||||||
blob,
|
|
||||||
url: URL.createObjectURL(blob),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// IndexedDB utility functions
|
|
||||||
export const openDB = () => {
|
|
||||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('MyDatabase', 1);
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
|
||||||
db.createObjectStore('backgrounds', { keyPath: 'id' });
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const writeData = async (fileId: string, type: string, blob: Blob) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
openDB().then(async (db) => {
|
|
||||||
const tx = db.transaction('backgrounds', 'readwrite');
|
|
||||||
const store = tx.objectStore('backgrounds');
|
|
||||||
const request = store.put({ id: fileId, type, blob });
|
|
||||||
|
|
||||||
await new Promise((res, rej) => {
|
|
||||||
tx.oncomplete = () => res(request.result);
|
|
||||||
tx.onerror = () => rej(tx.error);
|
|
||||||
}).then(resolve, reject);
|
|
||||||
|
|
||||||
}).catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const readAllData = async (): Promise<Background[]> => {
|
|
||||||
const db = await openDB();
|
|
||||||
const tx = db.transaction('backgrounds', 'readonly');
|
|
||||||
const store = tx.objectStore('backgrounds');
|
|
||||||
const request = store.getAll();
|
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
type BackgroundUpdateCallback = () => void;
|
||||||
|
|
||||||
|
class BackgroundUpdates {
|
||||||
|
private static instance: BackgroundUpdates;
|
||||||
|
private listeners: Set<BackgroundUpdateCallback> = new Set();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): BackgroundUpdates {
|
||||||
|
if (!BackgroundUpdates.instance) {
|
||||||
|
BackgroundUpdates.instance = new BackgroundUpdates();
|
||||||
|
}
|
||||||
|
return BackgroundUpdates.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(callback: BackgroundUpdateCallback): void {
|
||||||
|
this.listeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(callback: BackgroundUpdateCallback): void {
|
||||||
|
this.listeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerUpdate(): void {
|
||||||
|
this.listeners.forEach(callback => callback());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backgroundUpdates = BackgroundUpdates.getInstance();
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
type SettingsPopupCallback = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a singleton that triggers an update when the settings popup is closed.
|
||||||
|
* This is used to close the colour picker.
|
||||||
|
* Usage:
|
||||||
|
* settingsPopup.addListener(() => {
|
||||||
|
* console.log('Settings popup closed');
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
class SettingsPopup {
|
||||||
|
private static instance: SettingsPopup;
|
||||||
|
private listeners: Set<SettingsPopupCallback> = new Set();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): SettingsPopup {
|
||||||
|
if (!SettingsPopup.instance) {
|
||||||
|
SettingsPopup.instance = new SettingsPopup();
|
||||||
|
}
|
||||||
|
return SettingsPopup.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(callback: SettingsPopupCallback): void {
|
||||||
|
this.listeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(callback: SettingsPopupCallback): void {
|
||||||
|
this.listeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerClose(): void {
|
||||||
|
this.listeners.forEach(callback => callback());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsPopup = SettingsPopup.getInstance();
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import browser from 'webextension-polyfill'
|
|
||||||
import { CustomTheme, DownloadedTheme, ThemeList } from '../types/CustomThemes';
|
|
||||||
import localforage from 'localforage';
|
|
||||||
|
|
||||||
export const setTheme = async (themeID: string) => {
|
|
||||||
// send message to the background script
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'SetTheme',
|
|
||||||
body: {
|
|
||||||
themeID: themeID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDownloadedThemes = async (): Promise<DownloadedTheme[]> => {
|
|
||||||
// send message to the background script
|
|
||||||
const response: DownloadedTheme[] = await new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
let availableThemes = await localforage.getItem('availableThemes') as string[];
|
|
||||||
availableThemes = Array.from(new Set(availableThemes));
|
|
||||||
|
|
||||||
const downloadedThemes: DownloadedTheme[] = [];
|
|
||||||
for (let i = 0; i < availableThemes.length; i++) {
|
|
||||||
let themeData = await localforage.getItem(availableThemes[i]) as DownloadedTheme;
|
|
||||||
|
|
||||||
downloadedThemes.push(themeData);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(downloadedThemes);
|
|
||||||
} catch(error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const listThemes = async (): Promise<ThemeList> => {
|
|
||||||
// send message to the background script
|
|
||||||
const response: ThemeList = await new Promise((resolve, reject) => {
|
|
||||||
browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'ListThemes'
|
|
||||||
}).then(async (response) => {
|
|
||||||
if (response) {
|
|
||||||
// convert the response themes coverImage to a bloburl
|
|
||||||
response.themes = await Promise.all(
|
|
||||||
response.themes.map(async (theme: Omit<CustomTheme, 'CustomImages'>) => {
|
|
||||||
if (theme.coverImage) {
|
|
||||||
const blob = await fetch(theme.coverImage as string).then((res) => res.blob());
|
|
||||||
theme.coverImage = URL.createObjectURL(blob);
|
|
||||||
}
|
|
||||||
return theme;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve(response);
|
|
||||||
} else {
|
|
||||||
reject(new Error('Failed to get response'));
|
|
||||||
}
|
|
||||||
}).catch((error: any) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const disableTheme = async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'DisableTheme',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteTheme = async (themeID: string) => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'DeleteTheme',
|
|
||||||
body: {
|
|
||||||
themeID: themeID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sendThemeUpdate = async (updatedTheme: CustomTheme | DownloadedTheme, saveTheme?: boolean, updateImages?: boolean, enableTheme?: boolean) => {
|
|
||||||
saveTheme = saveTheme || false;
|
|
||||||
enableTheme = enableTheme || false;
|
|
||||||
|
|
||||||
const imageDataPromises = updatedTheme.CustomImages.map(async (image) => {
|
|
||||||
if (saveTheme || updateImages) {
|
|
||||||
const base64 = await blobToBase64(image.blob);
|
|
||||||
return {
|
|
||||||
id: image.id,
|
|
||||||
variableName: image.variableName,
|
|
||||||
url: base64,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: image.id,
|
|
||||||
variableName: image.variableName,
|
|
||||||
url: ''
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(imageDataPromises).then(async (imageData) => {
|
|
||||||
const themeData = {
|
|
||||||
...updatedTheme,
|
|
||||||
CustomImages: imageData,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (saveTheme && updatedTheme.coverImage) {
|
|
||||||
themeData.coverImage = await blobToBase64(updatedTheme.coverImage as Blob);
|
|
||||||
} else {
|
|
||||||
themeData.coverImage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'UpdateThemePreview',
|
|
||||||
body: themeData,
|
|
||||||
save: saveTheme,
|
|
||||||
enableTheme: enableTheme
|
|
||||||
});
|
|
||||||
|
|
||||||
if (saveTheme) {
|
|
||||||
browser.runtime.sendMessage({ type: 'currentTab', info: 'CloseThemeCreator' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to convert a Blob to base64
|
|
||||||
const blobToBase64 = (blob: Blob): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const base64 = reader.result as string;
|
|
||||||
resolve(base64);
|
|
||||||
};
|
|
||||||
reader.onerror = (error) => {
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enableCurrentTheme = async () => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'EnableCurrentTheme',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveUpdatedTheme = async (updatedTheme: CustomTheme) => {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
type: 'currentTab',
|
|
||||||
info: 'SaveTheme',
|
|
||||||
body: updatedTheme,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
type ThemeUpdateCallback = () => void;
|
||||||
|
|
||||||
|
class ThemeUpdates {
|
||||||
|
private static instance: ThemeUpdates;
|
||||||
|
private listeners: Set<ThemeUpdateCallback> = new Set();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): ThemeUpdates {
|
||||||
|
if (!ThemeUpdates.instance) {
|
||||||
|
ThemeUpdates.instance = new ThemeUpdates();
|
||||||
|
}
|
||||||
|
return ThemeUpdates.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(callback: ThemeUpdateCallback): void {
|
||||||
|
this.listeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(callback: ThemeUpdateCallback): void {
|
||||||
|
this.listeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerUpdate(): void {
|
||||||
|
this.listeners.forEach(callback => callback());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeUpdates = ThemeUpdates.getInstance();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export let selectedBackground = $state<string | null>(null);
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import browser from 'webextension-polyfill'
|
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { SettingsProps } from "../types/SettingsProps";
|
|
||||||
import { SettingsState } from "../types/AppProps";
|
|
||||||
import { SettingsState as StorageSettingsState } from '../../types/storage';
|
|
||||||
|
|
||||||
let RanOnce = false;
|
|
||||||
let previousSettingsState: SettingsState
|
|
||||||
|
|
||||||
const useSettingsState = ({ settingsState, setSettingsState }: SettingsProps) => {
|
|
||||||
useEffect(() => {
|
|
||||||
if (RanOnce) return;
|
|
||||||
RanOnce = true;
|
|
||||||
|
|
||||||
// @ts-expect-error - TODO: Fix this
|
|
||||||
browser.storage.local.get().then((result: StorageSettingsState) => {
|
|
||||||
setSettingsState({
|
|
||||||
notificationCollector: result.notificationcollector,
|
|
||||||
lessonAlerts: result.lessonalert,
|
|
||||||
animatedBackground: result.animatedbk,
|
|
||||||
animatedBackgroundSpeed: result.bksliderinput,
|
|
||||||
customThemeColor: result.selectedColor,
|
|
||||||
betterSEQTAPlus: result.onoff,
|
|
||||||
shortcuts: result.shortcuts,
|
|
||||||
customshortcuts: result.customshortcuts,
|
|
||||||
transparencyEffects: result.transparencyEffects,
|
|
||||||
selectedTheme: result.selectedTheme,
|
|
||||||
timeFormat: result.timeFormat,
|
|
||||||
animations: result.animations,
|
|
||||||
defaultPage: result.defaultPage,
|
|
||||||
devMode: result.devMode || false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const keyToStateMap = useMemo(() => ({
|
|
||||||
"notificationcollector": "notificationCollector",
|
|
||||||
"lessonalert": "lessonAlerts",
|
|
||||||
"animatedbk": "animatedBackground",
|
|
||||||
"bksliderinput": "animatedBackgroundSpeed",
|
|
||||||
"selectedColor": "customThemeColor",
|
|
||||||
"onoff": "betterSEQTAPlus",
|
|
||||||
"shortcuts": "shortcuts",
|
|
||||||
"customshortcuts": "customshortcuts",
|
|
||||||
"transparencyEffects": "transparencyEffects",
|
|
||||||
"selectedTheme": "selectedTheme",
|
|
||||||
"timeFormat": "timeFormat",
|
|
||||||
"animations": "animations",
|
|
||||||
"defaultPage": "defaultPage",
|
|
||||||
"devMode": "devMode"
|
|
||||||
}), []);
|
|
||||||
|
|
||||||
const storageChangeListener = (changes: browser.Storage.StorageChange) => {
|
|
||||||
for (const [key, { newValue }] of Object.entries(changes)) {
|
|
||||||
if (key === "DarkMode") {
|
|
||||||
if (key === "DarkMode" && newValue) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error - TODO: Fix this
|
|
||||||
const stateKey = keyToStateMap[key as keyof StorageSettingsState];
|
|
||||||
if (stateKey) {
|
|
||||||
setSettingsState((prevState: SettingsState) => ({
|
|
||||||
...prevState,
|
|
||||||
[stateKey]: newValue
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
browser.storage.onChanged.addListener(storageChangeListener);
|
|
||||||
return () => {
|
|
||||||
browser.storage.onChanged.removeListener(storageChangeListener);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const setStorage = (key: keyof StorageSettingsState, value: any) => {
|
|
||||||
browser.storage.local.set({ [key]: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (previousSettingsState) {
|
|
||||||
for (const [key, value] of Object.entries(settingsState)) {
|
|
||||||
// @ts-expect-error - TODO: Fix this
|
|
||||||
const storageKey = Object.keys(keyToStateMap).find(k => keyToStateMap[k] === key);
|
|
||||||
// @ts-expect-error - TODO: Fix this
|
|
||||||
if (storageKey && value !== previousSettingsState[key]) {
|
|
||||||
setStorage(storageKey as keyof StorageSettingsState, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
previousSettingsState = settingsState;
|
|
||||||
}, [settingsState, keyToStateMap])
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useSettingsState;
|
|
||||||
+41
-8
@@ -1,17 +1,50 @@
|
|||||||
|
@import './components/ColourPicker.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
button {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
@apply cursor-pointer;
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-width {
|
||||||
|
width: var(--tab-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
&:focus {
|
||||||
|
box-shadow: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar {
|
||||||
|
scrollbar-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorHeight {
|
||||||
|
height: calc(100vh - 58px);
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BetterSEQTA+ Settings</title>
|
<title>BetterSEQTA+ Settings</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="dark:bg-zinc-900">
|
<body class="h-[600px]">
|
||||||
<div id="ExtensionPopup"></div>
|
<div id="app" style="height: 100%;"></div>
|
||||||
<script type="module" src="./main.tsx"></script>
|
<script type="module" src="./index.ts"></script>
|
||||||
<script type="module" src="./dark.ts"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user