Compare commits

...

209 Commits

Author SHA1 Message Date
Seth Burkart 8e11ab821b Merge pull request #187 from BetterSEQTA/amended-branch
Amended branch
2024-11-15 11:36:23 +11:00
SethBurkart123 cca59ebf06 feat: new update video 2024-11-15 11:32:57 +11:00
SethBurkart123 14648b70f5 fix: settingsPopup not correctly opening when animations are disabled 2024-11-15 11:32:45 +11:00
Seth Burkart 0184e90088 feat: new update video 2024-11-15 11:27:38 +11:00
Seth Burkart aa977a259d fix: settingsPopup not correctly opening when animations are disabled 2024-11-15 11:10:31 +11:00
Seth Burkart 1507e9cdfe Merge pull request #186 from ar-cyber/main
final touches
2024-11-13 11:49:17 +11:00
Andrew R c816174aed fix: readme a bit outdated 2024-11-13 10:09:16 +10:30
Andrew R 85dee0e2c1 fix: anything below 3.4 is not usable 2024-11-13 10:08:30 +10:30
Seth Burkart bebe0ac9b2 Merge pull request #155 from BetterSEQTA/svelte
Change Settings UI to svelte under the hood
2024-11-13 09:46:22 +11:00
sethburkart123 f4e8c68ef3 bump: version and update changelog 2024-11-13 09:46:09 +11:00
sethburkart123 8d1168d6c4 perf: move background updates to end of migration loop 2024-11-13 09:36:11 +11:00
sethburkart123 5dc3526711 fix: activate old background while on page 2024-11-13 09:34:34 +11:00
sethburkart123 0bb4d89570 feat: add selected background to background migration 2024-11-13 09:30:36 +11:00
sethburkart123 e5c05c0dca feat: complete migration logic 2024-11-13 09:27:14 +11:00
sethburkart123 172021d0d0 feat: background.ts loading and migration logic 2024-11-12 10:19:47 +11:00
sethburkart123 f9f7b54adc fix(CoverSwiper): add aria labels to buttons 2024-11-03 10:54:54 +11:00
sethburkart123 44126b6ee6 chore(svelte): update svelte to 5.0 2024-11-01 18:08:24 +11:00
sethburkart123 3ca9cf4415 style(store): improve styling of store 2024-11-01 18:06:45 +11:00
sethburkart123 be1336a9ec feat: switch store carousel to embla 2024-11-01 18:01:07 +11:00
sethburkart123 34185e61ff chore: remove @million/lint 2024-11-01 17:46:10 +11:00
Seth Burkart 426e0749ef Merge pull request #184 from BetterSEQTA/main
Merge main into svelte
2024-11-01 17:43:41 +11:00
sethburkart123 bb6de3a1a2 fix(types): add vite types to tsconfig.json 2024-11-01 17:42:38 +11:00
sethburkart123 9de6e8feaf feat: move svelte interface to 'src/interface' 2024-11-01 17:37:20 +11:00
sethburkart123 fe82365c24 chore(Old-Popup): remove old popup code 2024-11-01 17:28:56 +11:00
sethburkart123 60e7dad261 chore(Popup): remove unused console logs 2024-11-01 17:28:14 +11:00
sethburkart123 f8caf9cb35 fix(Popup): animations playing incorrectly 2024-11-01 17:13:01 +11:00
sethburkart123 1a17f91f10 fix(Popup): theme settings hidden in SEQTA + animations fixes 2024-11-01 17:05:02 +11:00
sethburkart123 043c466d9e fix(Popup): theme settings hidden in SEQTA 2024-11-01 16:38:14 +11:00
sethburkart123 3d458a185e fix(Popup): hide theme settings on external popup (as variables can't be accessed from there) 2024-11-01 16:37:30 +11:00
sethburkart123 58d3172c5d feat(Popup): hide buttons on standalone popup 2024-11-01 16:34:00 +11:00
sethburkart123 ab3d4b212c fix(Popup): incorrect rendering of text 2024-11-01 16:33:11 +11:00
sethburkart123 68c94f80d6 fix: text inside of app 2024-11-01 16:32:44 +11:00
sethburkart123 2627204112 fix: standalone not correctly being set 2024-11-01 16:31:23 +11:00
sethburkart123 54e7b58794 feat: make svelte interface work in popup 2024-11-01 11:35:24 +11:00
sethburkart123 3d276e3b22 fix: increase position of appendBackgroundToUI to handle larger files 2024-11-01 11:14:59 +11:00
sethburkart123 0671a7370b perf: add requestIdleCallback for rendering the svelte interface 2024-11-01 11:03:42 +11:00
sethburkart123 4a7cd9b7a1 chore: fix errors 2024-10-31 17:08:48 +11:00
sethburkart123 f2b299cc9c fix: colourpicker state not loading correctly 2024-10-31 17:05:30 +11:00
sethburkart123 c1e1741b71 feat: add fuse.js to store search 2024-10-31 16:54:02 +11:00
sethburkart123 bfb253341e feat: improved scroll in store 2024-10-31 16:24:21 +11:00
Seth Burkart d91d67cf15 Merge pull request #180 from ar-cyber/patch-11
fix: update security to accompany new changes
2024-10-28 11:39:55 +11:00
sethburkart123 8b672f3e67 fix: disallow invalid video file types 2024-10-27 22:15:53 +11:00
sethburkart123 bf1f7bfb3b feat: add backgrounds tab + filtering improvements 2024-10-27 19:10:13 +11:00
sethburkart123 34c88b06e8 fix: update manifest to remove unused permissions 2024-10-20 14:11:06 +11:00
sethburkart123 528b8b49a1 Refac: theme modal and header components 2024-10-20 11:30:22 +11:00
sethburkart123 1ad1a758fa merge changes from main to svelte 2024-10-20 11:29:11 +11:00
sethburkart123 cdfa6723ef fix: add patchPackage to fix Chrome 130+ compatability 2024-10-20 11:24:30 +11:00
sethburkart123 052c06a04d bump: version + fixes 2024-10-20 11:19:50 +11:00
Seth Burkart 75f5f698da Merge pull request #182 from ar-cyber/main
fix complete copy of css in some scss files
2024-10-18 11:36:40 +11:00
Andrew R 87b374571e fix: update security to accompany new changes 2024-10-18 10:34:10 +10:30
Andrew R 1fe4c71656 Merge pull request #6 from ar-cyber/patch-10
Patch 10
2024-10-14 06:54:17 +10:30
Andrew R b45b5cb22f fix: complete copy of code in css 2024-10-14 06:36:02 +10:30
Alphons Joseph e55e4c8a06 Update README.md 2024-10-11 09:14:43 +08:00
sethburkart123 a5098d62ed style(ThemePage): update editmode styles to look better 2024-10-08 19:03:20 +11:00
sethburkart123 f5462495c1 feat(ThemePage): add edit mode button 2024-10-08 18:50:57 +11:00
sethburkart123 b70a3e9268 fix(SaveTheme): images not saving properly 2024-10-07 16:56:07 +11:00
sethburkart123 cc6091c899 fix(AboutPage): links invalid after merge 2024-10-07 16:05:01 +11:00
sethburkart123 6153861f54 feat(AboutPage): improve styles and add image heading 2024-10-07 16:03:07 +11:00
sethburkart123 a8987b5a7b feat(AboutPage): improved about page with branding images 2024-10-07 15:50:30 +11:00
sethburkart123 e1544b1ee5 style(SettingsPopup): add icon to about button + minor style improvements 2024-10-07 15:42:00 +11:00
sethburkart123 b404828f0e fix(ThemeCreator): coverimage not loading on edit 2024-10-07 15:35:56 +11:00
SethBurkart123 83b0e8de3f feat: various improvements to styles and bugs 2024-10-06 10:03:15 +11:00
SethBurkart123 922a3d5837 feat(ThemeCreator): update colour picker on edit 2024-10-06 09:34:54 +11:00
SethBurkart123 386445c7ee fix(CodeEditor): rerender code editor on edit 2024-10-06 09:14:39 +11:00
SethBurkart123 7555b0ff14 fix(ThemeCreator): saving and editing with images not working 2024-10-05 21:38:52 +10:00
SethBurkart123 6597ff9075 feat(themeCreator): disable forcedark by default 2024-10-05 20:39:37 +10:00
SethBurkart123 84eaecdadd fix(PageDetection): SEQTA.ts throwing errors on html pages with less than 2 document childNodes 2024-10-05 20:22:39 +10:00
SethBurkart123 818ff48a0d feat(ThemePreview): update to follow blob format 2024-10-05 20:17:31 +10:00
SethBurkart123 33e34a0552 style(TabbedContainer): remove border from selected buttons 2024-10-05 19:21:12 +10:00
SethBurkart123 d4a1fe199a style(ThemeCreator): improved light + dark mode styles 2024-10-04 14:05:06 +10:00
SethBurkart123 2eefeb30b6 feat(ThemeCreator): add force theme toggle 2024-10-04 13:56:13 +10:00
SethBurkart123 e55fb35bf9 feat(ThemeCreator): add accordian menu's to toggle large settings 2024-10-04 13:18:33 +10:00
SethBurkart123 008666d81d refactor: remove unused CSS code in index.css 2024-10-04 12:34:50 +10:00
SethBurkart123 8b26947fcf feat: add max-height to cm-editor in index.css 2024-10-04 12:34:41 +10:00
SethBurkart123 d7a38a273c feat(themeCreator): add save theme button 2024-10-04 12:29:36 +10:00
SethBurkart123 a140bf02e0 feat(ThemeCreator): add image uploading 2024-10-04 12:24:43 +10:00
SethBurkart123 d624e9df32 fix: icon fonts not loading in inputs 2024-10-04 12:24:36 +10:00
sethburkart123 881339d016 fix(demo): font demo not loading correct font file 2024-10-04 12:07:49 +10:00
sethburkart123 096c53b359 chore: add style.css file for font icons demo page 2024-10-04 12:06:07 +10:00
sethburkart123 3440e86e2e chore: add source files for font icons demo 2024-10-04 12:03:10 +10:00
sethburkart123 43ff5d1037 feat: dev progess with icons 2024-10-02 19:29:00 +10:00
sethburkart123 0106124a60 fix(theme): handle undefined presets on exit in colour picker 2024-10-02 09:41:51 +10:00
sethburkart123 caa92e1f67 fix(theme): fix gradients for colour picker 2024-10-02 09:41:13 +10:00
sethburkart123 278a085286 feat(theme): implement custom state for theme creator colour picker 2024-10-02 09:39:06 +10:00
sethburkart123 2d325f820d feat: add codeMirror extensions correctly 2024-10-02 09:21:17 +10:00
sethburkart123 53cfb27899 feat(CodeEditor): enable dropcursor 2024-10-02 09:21:02 +10:00
sethburkart123 b6e91b2254 feat(CodeEditor): migrate to svelte code editor 2024-10-01 20:09:39 +10:00
codefactor-io 8b0de97bc6 [CodeFactor] Apply fixes to commit 49d4f39 2024-10-01 09:50:40 +00:00
sethburkart123 49d4f39584 fix(colourPicker): saving presets list at incorrect times 2024-10-01 19:50:28 +10:00
sethburkart123 1360736dd7 fix(colourPicker): recursively rerendering itself 2024-09-29 12:08:55 +10:00
sethburkart123 e49849c18a feat(settings): auto close popup when opening theme creator 2024-09-20 16:03:57 +10:00
sethburkart123 6267a77a71 feat(themeCreator): add svelte theme creator 2024-09-20 10:37:26 +10:00
sethburkart123 548dcbf34e chore(deps): remove unused dependencies 2024-09-19 17:47:35 +10:00
sethburkart123 92c0076e2d feat(Settings): add dev mode toggle to images 2024-09-19 17:03:58 +10:00
sethburkart123 91ec33c0f9 feat(Settings): add missing settings 2024-09-19 16:49:04 +10:00
sethburkart123 384663912d perf(ExtensionPopup): make extension popup animation buttery smooth 2024-09-19 16:40:33 +10:00
sethburkart123 a0082dc895 feat: Add settings popup close trigger to improve user experience 2024-09-19 16:21:31 +10:00
sethburkart123 03ffe22fbb feat(ColourPicker): add dark mode to the colour picker 2024-09-19 15:58:06 +10:00
sethburkart123 8f5013d2ff feat(ReactAdaptor): upgrade react to react 18 2024-09-19 14:54:48 +10:00
sethburkart123 831853798e chore(ui): add react gradient colour picker 2024-09-19 14:47:35 +10:00
sethburkart123 90a4c8f048 feat(performance): reduce transition animations 2024-09-18 09:07:25 +10:00
sethburkart123 c9550d0d37 feat(settings_sync): add syncing with store 2024-09-18 09:00:40 +10:00
sethburkart123 2a9e901b2b refactor: Move close button to store header 2024-09-17 18:11:38 +10:00
sethburkart123 f65dc92490 refactor: Improve tabbed container and add active class to selected tab 2024-09-17 17:59:00 +10:00
sethburkart123 fea486aa52 perf(BackgroundSelector): load settings image selector images on hover 2024-09-17 17:58:40 +10:00
sethburkart123 31e833f791 chore: remove unused background downloader circle component 2024-09-17 17:42:41 +10:00
sethburkart123 d0a7749006 chore(ThemeModal): remove unused imports 2024-09-17 17:41:33 +10:00
sethburkart123 94684a9481 fix(TabbedContainer): fix scrolling height on tabs 2024-09-17 17:40:53 +10:00
sethburkart123 580cd9b3d9 refactor: Update store page header styling, add search functionality, and improve theme management 2024-09-17 17:36:21 +10:00
Alphons Joseph ae10f334f1 forked theme generator and placed within betterseqta organisation 2024-09-15 19:45:57 +08:00
Seth Burkart 3b62a82d91 Merge pull request #173 from BetterSEQTA/main
Bring svelte up to date with main
2024-09-15 08:58:59 +10:00
sethburkart123 92fd20c380 refactor: Update store page header styling and add search functionality 2024-09-15 08:55:26 +10:00
sethburkart123 0b9240a390 feat: Add header component to store page for search functionality 2024-09-15 08:50:31 +10:00
sethburkart123 6885ae2d08 fix: Set display theme on cover swiper slide click 2024-09-15 08:39:31 +10:00
Seth Burkart f9c3fbcc87 Merge pull request #172 from ar-cyber/patch-8
add debug messages
2024-09-15 05:59:48 +10:00
Alphons Joseph b8c99baf0c load theme upon installing 2024-09-14 08:40:59 +08:00
Alphons Joseph 31cd9d0e48 Downloading themes @SethBurkart123 please check 2024-09-13 23:49:06 +08:00
Alphons Joseph f5119ac9ca Merge branch 'svelte' of https://github.com/BetterSEQTA/BetterSEQTA-Plus into svelte 2024-09-13 21:17:01 +08:00
Alphons Joseph 22d1f50372 bump packages 2024-09-13 21:16:41 +08:00
Andrew R 3819fb39c8 console.log => console.info. 2024-09-13 19:48:20 +09:30
sethburkart123 3388281744 fix(browser): fixed settings interface in firefox #166 2024-09-13 17:36:31 +10:00
sethburkart123 1272c60a4d feat(Store): added store page 2024-09-13 16:01:48 +10:00
Andrew R 3e851b335b incl: add a debug message to indicate end of init 2024-09-13 15:03:10 +09:30
Alphons Joseph 8e7782b6a1 Merge pull request #170 from ar-cyber/patch-6
fix bug template
2024-09-13 11:43:25 +08:00
Andrew R 263a7f3cda Merge pull request #5 from ar-cyber/patch-7
fix bug and move stuff to pr branch
2024-09-13 11:36:46 +09:30
Andrew R 17228e0444 add vulnerability template 2024-09-13 11:34:57 +09:30
Andrew R 3cd2f4ede8 bad copy of files 2024-09-13 11:32:56 +09:30
Andrew R e55fd65590 fix bug template 2024-09-13 11:30:52 +09:30
Alphons Joseph 1ff14b8f3e Merge pull request #169 from ar-cyber/patch-3
incl: make the description more appealing
2024-09-13 09:59:10 +08:00
Alphons Joseph db120c00ce keep safari 2024-09-13 09:58:25 +08:00
Andrew R 1aa63274e7 Merge pull request #4 from ar-cyber/main
merge some changes when it comes to issue templates
2024-09-13 10:26:24 +09:30
Andrew R 08c07db6cc redact: remove "by Nulkem 2024-09-13 10:23:32 +09:30
Andrew R 8d05692a9b Create SECURITY.md 2024-09-13 09:57:14 +09:30
Andrew R 538b46dac4 add feature request template 2024-09-13 09:48:18 +09:30
Andrew R c727bc668b make bug issue template 2024-09-13 09:47:35 +09:30
Andrew R fec555d220 incl: make the description more appealing 2024-09-13 09:32:44 +09:30
Seth Burkart 02a1e8e32d Merge pull request #168 from ar-cyber/patch-3
"npm run package" got removed ages ago and was replaced by "npm run zip"
2024-09-13 07:41:39 +10:00
Andrew R 4187a9cade "npm run package" got removed ages ago and was replaced by "npm run zip".
Still requires 7-Zip.
2024-09-13 06:41:33 +09:30
Alphons Joseph 8d08354f6c Merge pull request #167 from BetterSEQTA/main
Merge doc changes in main back into svelte branch
2024-09-12 13:57:14 +08:00
Alphons Joseph 15c6283bcf fix(readme): missing close parenthesis 2024-09-12 13:55:30 +08:00
Alphons Joseph de1b0c3194 Update README.md 2024-09-12 12:42:39 +08:00
Alphons Joseph 6da4c791a6 Merge branch 'svelte' of https://github.com/BetterSEQTA/BetterSEQTA-Plus into svelte 2024-09-12 12:24:40 +08:00
Alphons Joseph d1466da58b fix (UI - Firefox): CSS being inconsistently applied 2024-09-12 12:24:17 +08:00
Andrew R 01fc068bcd Update README.md 2024-09-12 13:38:51 +09:30
Alphons Joseph f08b851846 Merge pull request #163 from ar-cyber/patch-3
Patch missing or useless files.
2024-09-12 12:03:16 +08:00
Andrew R 10e8bc29df add the old file back 2024-09-12 13:27:40 +09:30
Andrew R 40650f902d DANGER, Will Robinson.
Pretty dangerous - could cause some legal trouble. Delete this for the best.
2024-09-12 07:06:25 +09:30
Andrew R 99c342da85 Update README.md 2024-09-12 07:05:05 +09:30
Andrew R 5f6e0fa122 delete old contribute file 2024-09-12 07:03:44 +09:30
Andrew R ad8eb2a273 accidental comma on contribute md file 2024-09-12 07:00:59 +09:30
Andrew R df23c4f888 make github stop complaining because of no edit guidelines 2024-09-12 06:58:50 +09:30
Seth Burkart ed6757edb1 Merge pull request #161 from ar-cyber/patch-5
ar-cyber svelte patches
2024-09-11 16:56:29 +10:00
Andrew R f1ba486dbf Update README.md 2024-09-11 13:37:37 +09:30
Alphons Joseph 531c350f06 Got rid of redundant CSS line 2024-09-11 09:33:15 +08:00
Alphons Joseph 2a91e7056e Just minor changes to readme, looks good 2024-09-11 09:29:02 +08:00
Alphons Joseph a04d8211ed Fix CSS comment style 2024-09-11 09:28:20 +08:00
Alphons Joseph 9f8816f322 Update SEQTA.ts - No release candidate has been created as of yet 2024-09-11 09:20:08 +08:00
Andrew R b55e8b47a0 incl: comment updates 2024-09-11 01:18:04 +00:00
Andrew R e8c0f075ec fix: update whats new page to accomodate new changes 2024-09-11 01:13:32 +00:00
Andrew R b4ca961392 incl: add warning message to readme 2024-09-11 00:26:52 +00:00
Andrew R 7fbdf8bf32 fix build error 2024-09-11 00:23:58 +00:00
Andrew R 8ccbdd49e1 fix: bad styling 2024-09-11 09:44:11 +09:30
Andrew R 64ffc462d0 fix: make proper about page 2024-09-11 09:10:15 +09:30
Alphons Joseph f985f9445f refactor (ui): change about page to icon next to what's new 2024-09-10 21:18:37 +08:00
sethburkart123 966b58b932 refactor(ui): extract about page opening to a direct function call 2024-09-10 09:43:43 +10:00
codefactor-io 2348b90023 [CodeFactor] Apply fixes to commit d68ba15 2024-09-09 10:17:47 +00:00
Alphons Joseph d68ba1521a About page button 2024-09-09 18:16:59 +08:00
sethburkart123 f6a58cda0f chore(logs): remove redundant console.log statements 2024-09-09 18:04:04 +10:00
sethburkart123 7822d210b2 feat(ui/settings): enhance darkmode styles for selected background items 2024-09-09 18:03:32 +10:00
sethburkart123 2d23669aa3 fix(ui/settings): properly set selectedBackground 2024-09-09 18:01:06 +10:00
sethburkart123 7951358cd0 refactor(component): abstract component selection from svelteRenderer function 2024-09-09 17:57:54 +10:00
sethburkart123 7ca4682adb fix(import): stop importing unused 'react' and 'million' 2024-09-09 17:48:23 +10:00
sethburkart123 d04965db6a chore(code): remove useless code comments 2024-09-09 17:47:22 +10:00
sethburkart123 ae1b676fc3 chore(tsconfig): add 'noEmitOnError' option to tsconfig.json 2024-09-09 17:45:47 +10:00
sethburkart123 6b20c13705 feat(ui/settings): lazyload background image and video swatches 2024-09-09 17:41:01 +10:00
sethburkart123 38ddcbf5ca feat(settings): Add light mode support to buttons in settings menu 2024-09-09 12:12:46 +10:00
sethburkart123 bff4a2abf4 fix(build): failing due to incorrect firefox popup file 2024-09-08 22:13:55 +10:00
sethburkart123 54d7eae95b chore(deps): remove unused dependencies 2024-09-08 22:04:02 +10:00
sethburkart123 fdeea2f626 feat(settings): add custom theme selector 2024-09-08 21:51:14 +10:00
codefactor-io 272deb2b8c [CodeFactor] Apply fixes to commit c3cb293 2024-09-08 09:33:59 +00:00
sethburkart123 c3cb2937c9 feat(settings): add background selector 2024-09-08 19:33:47 +10:00
sethburkart123 f0bdbbb14f refactor(settings): remove globalStandalone from settings interface 2024-09-08 10:03:05 +10:00
Alphons Joseph 52c8809cf2 fix bug 2024-09-07 00:03:56 +08:00
Alphons Joseph f95b845b92 fix: use crossenv and implement for windows building 2024-09-07 00:03:15 +08:00
Alphons Joseph a82cd79954 Merge branch 'svelte' of https://github.com/BetterSEQTA/BetterSEQTA-Plus into svelte 2024-09-06 22:39:32 +08:00
Alphons Joseph cd430f2027 on the way to get shortcuts working 2024-09-06 22:37:57 +08:00
sethburkart123 573ac401be feat(settings): add clear theme button 2024-09-06 17:35:01 +10:00
sethburkart123 ac73056128 refactor(settings): improve shortcuts button 2024-09-06 17:32:30 +10:00
sethburkart123 9363de5fb4 feat(settings): add working shortcuts and custom shortcuts 2024-09-06 17:29:07 +10:00
sethburkart123 7f93aef9cc fix(popup): correct incorrect transform on switches 2024-09-06 07:00:51 +10:00
Alphons Joseph 220d15ebbc refactor: add comment, remove unneeded function 2024-09-05 21:46:21 +08:00
Alphons Joseph 428ad7569e preliminary shortcut loading 2024-09-05 21:32:12 +08:00
Alphons Joseph 4a8ed32d3e remove redundant code 2024-09-05 20:45:52 +08:00
Alphons Joseph e001078808 easter egg 2024-09-05 20:08:43 +08:00
Alphons Joseph cf379c61aa remove redundant vite-project dir 2024-09-05 19:57:33 +08:00
Alphons Joseph e3ec0d83ab fix: Icons and images not displaying correctly 2024-09-05 19:54:46 +08:00
Alphons Joseph c2da4c1ed5 add chrome types 2024-09-05 19:48:09 +08:00
sethburkart123 d567c625c4 chore(deps): update motion to latest version 2024-09-04 16:30:56 +10:00
sethburkart123 52bbe4d7e4 feat(slider): update slider for Svelte 5 compatibility 2024-09-04 16:27:08 +10:00
sethburkart123 89f2743475 fix: tabs background width 0 2024-09-04 13:22:17 +10:00
sethburkart123 19102f9bcd fix: update props and types for General Settings page 2024-09-04 13:18:48 +10:00
sethburkart123 c008b32efa fix: svelte settings Sync 2024-09-04 09:42:07 +10:00
Seth Burkart c376183082 docs(readme): remove duplicate 'Creating Custom Themes' section 2024-09-03 16:38:43 +10:00
sethburkart123 e4ba89073c fix: logo broken 2024-09-02 21:48:17 +10:00
sethburkart123 2f08d6ee08 fix: building working, (lots of bugs) 2024-09-02 21:46:48 +10:00
sethburkart123 99a3166fa4 fix: failing path alias 2024-08-29 17:04:46 +10:00
sethburkart123 59a8084e98 fix: lib reference causing failing builds 2024-08-29 16:41:35 +10:00
sethburkart123 0d0e526a25 chore: update paths to be more absolute 2024-08-29 16:38:44 +10:00
sethburkart123 125ebfbaea refac: improve multi browser support 2024-08-29 16:28:56 +10:00
163 changed files with 8726 additions and 3722 deletions
+16
View File
@@ -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
View File
@@ -3,12 +3,12 @@
"browser": true,
"commonjs": true,
"es2021": true,
"node": true // add this line to allow Node.js-specific globals
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module" // add this line to allow 'import' and 'export' statements
"sourceType": "module"
},
"rules": {
// allow importing ts extensions
+27
View File
@@ -0,0 +1,27 @@
---
name: Report A bug.
about: Create a report of a present bug.
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Please indicate how did you make this happen.
**Expected behaviuor**
Please add a clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- If using Windows, the build number. Find this by using ```winver``` and copying down the build id.
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FR] "
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Vulnerability
about: Report a vulnerability in this extension.
title: "[VUL] "
labels: ''
assignees: ''
---
**What is the vulnerability?**
Describe the vulnerability in concise language.
**Where is the vulnerability found?**
Describe where the vulnerability is found.
**What does this affect?**
Explain what it affects. E.G: It opens up my school email to the world. etc.
+73
View File
@@ -0,0 +1,73 @@
name: MVP - make, version & publish
on:
push:
branches:
- main
workflow_dispatch: # This line adds manual triggering from the GitHub UI
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
make_version_publish:
name: Make, Version & Publish
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup Node 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install bun & Deps
run: |
npm install bun -g
bun install
- name: 'Build - all browsers'
id: buildProject
run: MODE=chrome vite build && MODE=firefox vite build
- name: '[ V E R S I O N ] : Create or Update Release Pull Request - Version Changes'
id: changesets
uses: changesets/action@v1
with:
version: bun run version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Get current version info from package.json'
if: steps.changesets.outputs.hasChangesets == 'false'
id: package
run: |
echo "::set-output name=PACKAGE_NAME::$(jq -r .name package.json)"
echo "::set-output name=PACKAGE_VERSION::$(jq -r .version package.json)"
working-directory: ${{ github.workspace }}
- name: 'Check if a git release already exists for current version'
if: steps.changesets.outputs.hasChangesets == 'false'
id: checkRelease
run: |
TAG_NAME=${{ steps.package.outputs.PACKAGE_NAME }}@${{ steps.package.outputs.PACKAGE_VERSION }}
if gh release view $TAG_NAME &>/dev/null; then
echo "Release $TAG_NAME already exists."
echo "RELEASE_EXISTS=true" >> $GITHUB_ENV
else
echo "RELEASE_EXISTS=false" >> $GITHUB_ENV
fi
- name: 'Create Release Archive(s) - zip 🫰 it 🫰 up 🫰 !'
id: zip
if: steps.changesets.outputs.hasChangesets == 'false'
run: bun run zip
- name: 'Create a git release w/ notes & release archive(s)'
id: gitRelease
if: steps.changesets.outputs.hasChangesets == 'false' && env.RELEASE_EXISTS != 'true'
run: bun run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PACKAGE_NAME: ${{ steps.package.outputs.PACKAGE_NAME }}
PACKAGE_VERSION: ${{ steps.package.outputs.PACKAGE_VERSION }}
+2 -11
View File
@@ -7,24 +7,15 @@ yarn.lock
.parcel-cache
.env
.env.submit
# Build
extension.zip
build/
dist/
betterseqtaplus-safari/
.million/
.vscode/
**/.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
View File
+8 -10
View File
@@ -1,3 +1,6 @@
#
<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" />
</a>
@@ -48,18 +51,13 @@
- Brave Supported
- Opera Supported
- Vivaldi Supported
- Chromium-based browsers are supported
- Firefox (Experimental - available [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)
- Safari (Experimental - only available via compilation)
## 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 :)
## 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 :)
@@ -111,7 +109,7 @@ npm run build
3. Package it up (optional)
```
npm run package # This requires 7-Zip to be installed in order to work
npm run zip # This requires 7-Zip to be installed in order to work
```
## Folder Structure
@@ -120,7 +118,7 @@ The folder structure is as follows:
- The `src` folder contains source files that are compiled to the build directory.
- 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.
@@ -130,7 +128,7 @@ The folder structure is as follows:
<img src="https://contrib.rocks/image?repo=betterseqta/betterseqta-plus" />
</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
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)
+16
View File
@@ -0,0 +1,16 @@
# 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.0 | :white_check_mark: |
| <= 3.3 | :x: |
`*` May not work on other devices.
## Reporting a Vulnerability
If you find vulnerabilities, REPORT IT IMMEDIATELY. Make an issue and use the template provided for vulnerabilities.
+16
View File
@@ -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}';`;
},
};
+33
View File
@@ -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
}
+65
View File
@@ -0,0 +1,65 @@
/*
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 **');
}
fs.watchFile(manifestPath, () => {
console.log('** watchFile ** ');
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 **');
}
}
});
});
},
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;
}
+90
View File
@@ -0,0 +1,90 @@
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+\.\d+\.\d+)-/);
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
return match ? match[1] : null;
}).filter(Boolean);
console.log('Extracted versions:', versions);
const latestVersion = semver.maxSatisfying(versions, '*');
console.log('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);
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);
+104
View File
@@ -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)
+21
View File
@@ -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]
}
-32
View File
@@ -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"
}
]
}
+61 -47
View File
@@ -1,15 +1,20 @@
{
"name": "betterseqtaplus",
"version": "3.3.0",
"version": "3.4.0",
"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, while incorporating a plethora of new and improved features!",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"dev": "vite dev",
"dev:firefox": "VITE_TARGET=firefox vite build --watch",
"build": "vite build",
"build:firefox": "VITE_TARGET=firefox vite build",
"package": "rimraf ./dist/*.map && 7z a -tzip extension.zip ./dist/*"
"dev": "cross-env MODE=chrome vite dev",
"dev:firefox": "cross-env MODE=firefox vite build --watch",
"build": "cross-env MODE=chrome vite build && cross-env MODE=firefox vite build",
"build:chrome": "cross-env MODE=chrome vite build",
"build:firefox": "cross-env MODE=firefox vite build",
"build:safari": "cross-env MODE=safari vite build",
"convert:safari": "xcrun safari-web-extension-converter dist/safari --project-location . --app-name $npm_package_name-safari",
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
"publish": "bun lib/publish.js --b",
"zip": "bedframe zip"
},
"targets": {
"prod": {
@@ -19,64 +24,73 @@
}
},
"keywords": [],
"author": "",
"author": {
"name": "SethBurkart123",
"email": "betterseqta@betterseqta.com",
"url": "https://github.com/BetterSEQTA/BetterSEQTA-plus"
},
"license": "MIT",
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.23",
"@crxjs/vite-plugin": "2.0.0-beta.25",
"@types/mime-types": "^2.1.4",
"@vitejs/plugin-react-swc": "^3.6.0",
"eslint": "^8.56.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"glob": "^11.0.0",
"mime-types": "^2.1.35",
"parcel": "^2.11.0",
"prettier": "^3.2.5",
"prettier": "^3.3.3",
"process": "^0.11.10",
"sass": "^1.70.0",
"sass": "^1.78.0",
"sass-loader": "^13.3.3",
"url": "^0.11.3"
"semver": "^7.6.3",
"url": "^0.11.4"
},
"dependencies": {
"@blocknote/core": "^0.14.1",
"@blocknote/mantine": "^0.14.1",
"@blocknote/react": "^0.14.1",
"@bedframe/cli": "^0.0.85",
"@codemirror/lang-css": "^6.3.0",
"@codemirror/lang-less": "^6.0.2",
"@heroicons/react": "^2.1.3",
"@million/lint": "latest",
"@tailwindcss/forms": "^0.5.7",
"@codemirror/theme-one-dark": "^6.1.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.0.270",
"@types/color": "^3.0.6",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.4",
"@types/node": "^20.11.30",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/sortablejs": "^1.15.7",
"@types/lodash": "^4.17.7",
"@types/node": "^20.16.5",
"@types/react": "17",
"@types/react-dom": "17",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8",
"@types/webextension-polyfill": "^0.10.7",
"@uiw/codemirror-extensions-color": "^4.21.25",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25",
"autoprefixer": "^10.4.17",
"@uiw/codemirror-extensions-color": "^4.23.3",
"@uiw/codemirror-theme-github": "^4.23.3",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"classnames": "^2.5.1",
"codemirror": "^6.0.1",
"color": "^4.2.3",
"dompurify": "^3.0.8",
"framer-motion": "^11.0.25",
"dompurify": "^3.1.6",
"embla-carousel-autoplay": "^8.3.1",
"embla-carousel-svelte": "^8.3.1",
"fuse.js": "^7.0.0",
"idb": "^8.0.0",
"kolorist": "^1.8.0",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"million": "latest",
"motion": "^10.17.0",
"react": "^18.2.0",
"react-best-gradient-color-picker": "3.0.5",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.13",
"react-router-dom": "^6.22.0",
"react-toastify": "^10.0.5",
"rimraf": "^5.0.5",
"sortablejs": "^1.15.2",
"swiper": "latest",
"tailwindcss": "^3.4.1",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"million": "^3.1.11",
"motion": "^10.18.0",
"postcss": "^8.4.45",
"publish-browser-extension": "^2.2.1",
"react": "17",
"react-best-gradient-color-picker": "^3.0.10",
"react-dom": "17",
"sortablejs": "^1.15.3",
"svelte": "^5.1.9",
"tailwindcss": "^3.4.11",
"typescript": "^5.6.2",
"uuid": "^9.0.1",
"vite": "^5.2.2",
"vite": "^5.4.4",
"webextension-polyfill": "^0.10.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+189 -112
View File
@@ -5,42 +5,40 @@ import browser from 'webextension-polyfill'
import { animate, spring, stagger } from 'motion'
// Internal utilities and functions
import { delay } from './seqta/utils/delay'
import stringToHTML from './seqta/utils/stringToHTML'
import { MessageHandler } from './seqta/utils/listeners/MessageListener'
import { initializeSettingsState, settingsState } from './seqta/utils/listeners/SettingsState'
import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges'
import { eventManager } from './seqta/utils/listeners/EventManager'
import { delay } from '@/seqta/utils/delay'
import stringToHTML from '@/seqta/utils/stringToHTML'
import { MessageHandler } from '@/seqta/utils/listeners/MessageListener'
import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState'
import { StorageChangeHandler } from '@/seqta/utils/listeners/StorageChanges'
import { eventManager } from '@/seqta/utils/listeners/EventManager'
// UI and theme management
import loading, { AppendLoadingSymbol } from './seqta/ui/Loading'
import { enableCurrentTheme } from './seqta/ui/themes/enableCurrent'
import { updateAllColors } from './seqta/ui/colors/Manager'
import { SettingsResizer } from './seqta/ui/SettingsResizer'
import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements'
import loading, { AppendLoadingSymbol } from '@/seqta/ui/Loading'
import { enableCurrentTheme } from '@/seqta/ui/themes/enableCurrent'
import { updateAllColors } from '@/seqta/ui/colors/Manager'
import { SettingsResizer } from '@/seqta/ui/SettingsResizer'
import { AddBetterSEQTAElements } from '@/seqta/ui/AddBetterSEQTAElements'
// JSON content
import MenuitemSVGKey from './seqta/content/MenuItemSVGKey.json'
import ShortcutLinks from './seqta/content/links.json'
import MenuitemSVGKey from '@/seqta/content/MenuItemSVGKey.json'
import ShortcutLinks from '@/seqta/content/links.json'
// Icons and fonts
import IconFamily from './resources/fonts/IconFamily.woff'
import LogoLight from './resources/icons/betterseqta-light-icon.png'
import LogoLightOutline from './resources/icons/betterseqta-light-outline.png'
import icon48 from './resources/icons/icon-48.png?base64'
import assessmentsicon from './seqta/icons/assessmentsIcon'
import coursesicon from './seqta/icons/coursesIcon'
import IconFamily from '@/resources/fonts/IconFamily.woff'
import LogoLight from '@/resources/icons/betterseqta-light-icon.png'
import LogoLightOutline from '@/resources/icons/betterseqta-light-outline.png'
import icon48 from '@/resources/icons/icon-48.png?base64'
import assessmentsicon from '@/seqta/icons/assessmentsIcon'
import coursesicon from '@/seqta/icons/coursesIcon'
// Stylesheets
import iframeCSS from './css/iframe.scss?raw'
import injectedCSS from './css/injected.scss?inline'
import documentLoadCSS from './css/documentload.scss?inline'
declare global {
interface Window {
chrome?: any
}
}
import iframeCSS from '@/css/iframe.scss?raw'
import injectedCSS from '@/css/injected.scss?inline'
import documentLoadCSS from '@/css/documentload.scss?inline'
import renderSvelte from '@/interface/main'
import Settings from '@/interface/pages/settings.svelte'
import { settingsPopup } from './interface/hooks/SettingsPopup'
import { migrateBackgrounds } from './seqta/utils/migrateBackgrounds'
let SettingsClicked = false
export let MenuOptionsOpen = false
@@ -48,10 +46,13 @@ let currentSelectedDate = new Date()
let LessonInterval: any
var IsSEQTAPage = false
let hasSEQTAText = false
// This check is placed outside of the document load event due to issues with EP (https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/84)
const hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software')
init()
if (document.childNodes[1]) {
hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software') ?? false
init()
}
async function init() {
CheckForMenuList()
@@ -59,7 +60,8 @@ async function init() {
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
IsSEQTAPage = true
console.log('[BetterSEQTA+] Verified SEQTA Page')
console.info('[BetterSEQTA+] Verified SEQTA Page')
const documentLoadStyle = document.createElement('style')
documentLoadStyle.textContent = documentLoadCSS
document.head.appendChild(documentLoadStyle)
@@ -83,7 +85,7 @@ async function init() {
document.head.appendChild(injectedStyle)
}
}
console.info('[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.')
main()
} catch (error: any) {
console.error(error)
@@ -110,7 +112,7 @@ async function HideMenuItems(): Promise<void> {
for (const [menuItem, { toggle }] of Object.entries(settingsState.menuitems)) {
if (!toggle) {
stylesheetInnerText += SetDisplayNone(menuItem)
console.log(`[BetterSEQTA+] Hiding ${menuItem} menu item`)
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`)
}
}
@@ -118,7 +120,7 @@ async function HideMenuItems(): Promise<void> {
menuItemStyle.innerText = stylesheetInnerText
document.head.appendChild(menuItemStyle)
} catch (error) {
console.error("An error occurred:", error)
console.error("[BetterSEQTA+] An error occurred:", error)
}
}
@@ -144,7 +146,7 @@ export function OpenWhatsNewPopup() {
let video = document.createElement('video')
let source = document.createElement('source')
// Perhaps we host this on a server and then grab it instead of having it locally?
source.setAttribute('src', 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/update-video.webm')
source.setAttribute('src', 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/update-video.mp4')
video.autoplay = true
video.muted = true
video.loop = true
@@ -158,6 +160,15 @@ export function OpenWhatsNewPopup() {
let text = stringToHTML(
/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: scroll;">
<h1>3.4.0 - Major Performance Update</h1>
<li>Completely rebuilt the extension popup using Svelte for dramatically improved performance</li>
<li>Added a brand new background store with search functionality and downloadable backgrounds</li>
<li>Significant code cleanup and optimization across the extension</li>
<li>Improved overall responsiveness and load times</li>
<li>Smoother animations and improved scrolling</li>
<li>Fixed Firefox compatibility issues</li>
<li>Other minor bug fixes and under the hood improvements</li>
<h1>3.3.1 - Hot Fix</h1>
<li>Fixed assessments not loading when no notices are available</li>
@@ -347,6 +358,102 @@ export function OpenWhatsNewPopup() {
})
}
export function OpenAboutPage() {
const background = document.createElement('div')
background.id = 'whatsnewbk'
background.classList.add('whatsnewBackground')
const container = document.createElement('div')
container.classList.add('whatsnewContainer')
var header: any = stringToHTML(
/* html */
`<div class="whatsnewHeader">
<h1>About</h1>
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
</div>`
).firstChild
let text = stringToHTML(
/* html */ `
<div class="whatsnewTextContainer" style="overflow-y: scroll;">
<img src="${settingsState.DarkMode ? 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/dark.jpg' : 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/light.jpg'}" class="aboutImg" />
<p>BetterSEQTA+ is a fork of BetterSEQTA which was originally developed by Nulkem, which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features. </p>
<p>We are currently working on fixing bugs and adding good features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below).</p>
<h1>Credits</h1>
<p>Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph and SethBurkart123.</p>
</div>
`,
).firstChild
let footer = stringToHTML(
/* html */ `
<div class="whatsnewFooter">
<div>
Report bugs and feedback:
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding:0;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25px" height="25px" viewBox="0 0 256 250" version="1.1" preserveAspectRatio="xMidYMid">
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
</svg>
</a>
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding:0;">
<svg style="width:25px;height:25px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</a>
</div>
</div>
`).firstChild
let exitbutton = document.createElement('div')
exitbutton.id = 'whatsnewclosebutton'
container.append(header)
container.append(text as ChildNode)
container.append(footer as ChildNode)
container.append(exitbutton)
background.append(container)
document.getElementById('container')!.append(background)
let bkelement = document.getElementById('whatsnewbk')
let popup = document.getElementsByClassName('whatsnewContainer')[0]
if (settingsState.animations) {
animate(
[popup, bkelement as HTMLElement],
{ scale: [0, 1], opacity: [0, 1] },
{ easing: spring({ stiffness: 220, damping: 18 }) }
)
animate(
'.whatsnewTextContainer *',
{ opacity: [0, 1], y: [10, 0] },
{
delay: stagger(0.05, { start: 0.1 }),
duration: 0.5,
easing: [.22, .03, .26, 1]
}
)
}
delete settingsState.justupdated
bkelement!.addEventListener('click', function (event) {
// Check if the click event originated from the element itself and not any of its children
if (event.target === bkelement) {
DeleteWhatsNew()
}
});
var closeelement = document.getElementById('whatsnewclosebutton')
closeelement!.addEventListener('click', function () {
DeleteWhatsNew()
})
}
export async function finishLoad() {
try {
document.querySelector('.legacy-root')?.classList.remove('hidden');
@@ -361,7 +468,10 @@ export async function finishLoad() {
if (settingsState.justupdated && !document.getElementById('whatsnewbk')) {
OpenWhatsNewPopup();
/* Background Migration script */
}
migrateBackgrounds();
}
async function DeleteWhatsNew() {
@@ -527,12 +637,9 @@ function applyDarkModeToIframe(iframe: HTMLIFrameElement, cssLink: HTMLStyleElem
const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return;
if (iframeDocument.readyState !== 'complete') {
iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink);
};
return;
}
iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink);
};
if (settingsState.DarkMode) {
iframeDocument.documentElement.classList.add('dark')
@@ -610,7 +717,7 @@ async function handleSublink(sublink: string | undefined): Promise<void> {
break;
case 'home':
window.location.replace(`${location.origin}/#?page=/home`);
console.log('[BetterSEQTA+] Started Init')
console.info('[BetterSEQTA+] Started Init')
if (settingsState.onoff) loadHomePage()
finishLoad();
break;
@@ -654,7 +761,7 @@ async function handleTimetable(): Promise<void> {
}
async function handleNewsPage(): Promise<void> {
console.log('[BetterSEQTA+] Started Init');
console.info('[BetterSEQTA+] Started Init');
if (settingsState.onoff) {
SendNewsPage();
if (settingsState.notificationcollector) {
@@ -873,7 +980,7 @@ function main() {
}
if (settingsState.onoff) {
console.log('[BetterSEQTA+] Enabled')
console.info('[BetterSEQTA+] Enabled')
if (settingsState.DarkMode) document.documentElement.classList.add('dark')
document.querySelector('.legacy-root')?.classList.add('hidden')
@@ -893,7 +1000,7 @@ function main() {
}
function InjectCustomIcons() {
console.log('[BetterSEQTA+] Injecting Icons')
console.info('[BetterSEQTA+] Injecting Icons')
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
@@ -908,7 +1015,7 @@ function InjectCustomIcons() {
}
export function AppendElementsToDisabledPage() {
console.log("[BetterSEQTA+] Appending elements to disabled page")
console.info("[BetterSEQTA+] Appending elements to disabled page")
AddBetterSEQTAElements()
let settingsStyle = document.createElement('style')
@@ -940,30 +1047,22 @@ export function AppendElementsToDisabledPage() {
document.head.append(settingsStyle)
}
export function closeSettings() {
const ExtensionSettings = document.getElementById('ExtensionPopup')!
const ExtensionIframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement
export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
if (!extensionPopup) extensionPopup = document.getElementById('ExtensionPopup')!
if (SettingsClicked == true) {
ExtensionSettings!.classList.add('hide')
if (settingsState.animations) {
animate(
'#ExtensionPopup',
{ opacity: [1, 0], scale: [1, 0] },
{ easing: spring({ stiffness: 220, damping: 18 }) }
)
} else {
ExtensionSettings.style.opacity = '0'
ExtensionSettings.style.transform = 'scale(0)'
}
SettingsClicked = false
if (ExtensionIframe.contentWindow) {
ExtensionIframe.contentWindow.postMessage('popupClosed', '*')
}
extensionPopup.classList.add('hide')
if (settingsState.animations) {
animate((progress) => {
extensionPopup.style.opacity = Math.max(0, 1 - progress).toString()
extensionPopup.style.transform = `scale(${Math.max(0, 1 - progress)})`
}, { easing: spring({ stiffness: 520, damping: 20 }) })
} else {
extensionPopup.style.opacity = '0'
extensionPopup.style.transform = 'scale(0)'
}
ExtensionSettings!.classList.add('hide')
settingsPopup.triggerClose()
SettingsClicked = false
}
export function addExtensionSettings() {
@@ -974,42 +1073,23 @@ export function addExtensionSettings() {
const extensionContainer = document.querySelector('#container') as HTMLDivElement
if (extensionContainer) extensionContainer.appendChild(extensionPopup)
const extensionIframe: HTMLIFrameElement = document.createElement('iframe')
extensionIframe.src = `${browser.runtime.getURL('src/interface/index.html')}#settings/embedded`
extensionIframe.id = 'ExtensionIframe'
extensionIframe.setAttribute('allowTransparency', 'true')
extensionIframe.setAttribute('excludeDarkCheck', 'true')
extensionIframe.style.width = '384px'
extensionIframe.style.height = '100%'
extensionIframe.style.border = 'none'
extensionPopup.appendChild(extensionIframe)
// create shadow dom and render svelte app
try {
const shadow = extensionPopup.attachShadow({ mode: 'open' });
requestIdleCallback(() => renderSvelte(Settings, shadow));
} catch (err) {
console.error(err)
}
const container = document.getElementById('container')
new SettingsResizer();
const closeExtensionPopup = () => {
const ExtensionIframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement
extensionPopup.classList.add('hide')
if (settingsState.animations) {
animate(
'#ExtensionPopup',
{ opacity: [1, 0], scale: [1, 0] },
{ easing: spring({ stiffness: 220, damping: 18 }) }
)
} else {
extensionPopup.style.opacity = '0'
extensionPopup.style.transform = 'scale(0)'
}
if (ExtensionIframe.contentWindow) {
ExtensionIframe.contentWindow.postMessage('popupClosed', '*')
}
SettingsClicked = false
}
container!.onclick = (event) => {
if ((event.target as HTMLElement).closest('#AddedSettings') == null && SettingsClicked) {
if (!SettingsClicked) return;
if (!(event.target as HTMLElement).closest('#AddedSettings')) {
if (event.target == extensionPopup) return;
closeExtensionPopup()
}
}
@@ -1244,25 +1324,22 @@ export function setupSettingsButton() {
var AddedSettings = document.getElementById('AddedSettings');
var extensionPopup = document.getElementById('ExtensionPopup');
AddedSettings!.addEventListener('click', function () {
AddedSettings!.addEventListener('click', async () => {
if (SettingsClicked) {
extensionPopup!.classList.add('hide');
if (settingsState.animations) {
animate('#ExtensionPopup', { opacity: [1, 0], scale: [1, 0] }, { easing: spring({ stiffness: 220, damping: 18 }) });
} else {
extensionPopup!.style.opacity = '0'
extensionPopup!.style.transform = 'scale(0)'
}
(document.getElementById('ExtensionIframe')! as HTMLIFrameElement).contentWindow!.postMessage('popupClosed', '*');
SettingsClicked = false;
closeExtensionPopup(extensionPopup as HTMLElement);
} else {
extensionPopup!.classList.remove('hide');
if (settingsState.animations) {
animate('#ExtensionPopup', { opacity: [0, 1], scale: [0, 1] }, { easing: spring({ stiffness: 260, damping: 24 }) });
animate((progress) => {
extensionPopup!.style.opacity = progress.toString()
extensionPopup!.style.transform = `scale(${progress})`
}, { easing: spring({ stiffness: 280, damping: 20 }) });
} else {
extensionPopup!.style.opacity = '1'
extensionPopup!.style.transform = 'scale(1)'
extensionPopup!.style.transition = 'opacity 0s linear, transform 0s linear'
}
extensionPopup!.classList.remove('hide');
SettingsClicked = true;
}
});
@@ -2033,7 +2110,7 @@ async function AddCustomShortcutsToPage() {
export async function loadHomePage() {
// Sends the html data for the home page
console.log('[BetterSEQTA+] Started Loading Home Page')
console.info('[BetterSEQTA+] Started Loading Home Page')
document.title = 'Home ― SEQTA Learn'
const element = document.querySelector('[data-key=home]')
@@ -2379,7 +2456,7 @@ export function enableNotificationCollector() {
'notifications__bubble___1EkSQ'
)[0]
if (typeof alertdiv == 'undefined') {
console.log('[BetterSEQTA+] No notifications currently')
console.info('[BetterSEQTA+] No notifications currently')
} else {
alertdiv.textContent = Notifications.payload.notifications.length
}
@@ -2429,7 +2506,7 @@ function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
export function SendNewsPage() {
setTimeout(function () {
// Sends the html data for the home page
console.log('[BetterSEQTA+] Started Loading News Page')
console.info('[BetterSEQTA+] Started Loading News Page')
document.title = 'News ― SEQTA Learn'
var element = document.querySelector('[data-key=news]')
+1 -1
View File
@@ -1,5 +1,5 @@
import browser from 'webextension-polyfill'
import { SettingsState } from "./types/storage";
import type { SettingsState } from "@/types/storage";
export const openDB = () => {
return new Promise((resolve, reject) => {
+151 -52
View File
@@ -1,9 +1,9 @@
@charset "UTF-8";
@import url('https://fonts.googleapis.com/css?family=Rubik:300,400,500,600');
@import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600");
@import './injected/sidebar-animation.scss';
@import './injected/theme.scss';
@import './injected/transparency.scss';
@import "./injected/sidebar-animation.scss";
@import "./injected/theme.scss";
@import "./injected/transparency.scss";
:root {
background: var(--better-main) !important;
@@ -16,7 +16,12 @@
}
body,
.legacy-root input, .legacy-root textarea, .legacy-root button, .legacy-root select, .legacy-root option, .legacy-root .input,
.legacy-root input,
.legacy-root textarea,
.legacy-root button,
.legacy-root select,
.legacy-root option,
.legacy-root .input,
html {
font-family: Rubik, sans-serif !important;
}
@@ -26,10 +31,6 @@ html {
}
* {
--theme-fg-parts: white;
transition:
background-color 200ms ease-in-out,
backdrop-filter 200ms ease-in-out;
}
.extension-editor {
background: var(--background-primary);
@@ -39,7 +40,8 @@ html {
height: 100%;
visibility: visible !important;
}
#themeCreatorIframe {
#themeCreator {
position: fixed;
right: 0;
height: 100%;
@@ -62,6 +64,40 @@ html {
}
}
#store {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9998;
animation: fadeIn 500ms forwards;
animation-delay: 200ms;
opacity: 0;
&.hide {
animation: fadeOut 500ms forwards;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dark .resizeBar {
background-color: rgb(28 28 30);
}
@@ -82,6 +118,10 @@ html {
z-index: 10000;
}
.modaliser-container {
backdrop-filter: none !important;
}
.connectedNotificationsWrapper > div > button > svg > g {
fill: var(--theme-primary) !important;
}
@@ -165,7 +205,8 @@ html {
background: unset;
}
.legacy-root button:active, .legacy-root a:active:not(.cke_combo_button) {
.legacy-root button:active,
.legacy-root a:active:not(.cke_combo_button) {
background-image: unset !important;
}
@@ -175,7 +216,8 @@ html {
}
.dark .dashboard section {
input, select {
input,
select {
background: rgba(255, 255, 255, 0.1);
}
}
@@ -197,7 +239,7 @@ html {
input,
select {
border: transparent;
background: rgba(0, 0, 0, .1);
background: rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
@@ -239,7 +281,7 @@ html {
color: var(--text-primary);
}
ul.magicDelete > li:hover {
background: rgba(0, 0, 0, .1);
background: rgba(0, 0, 0, 0.1);
}
.dashlet-notes > .editor {
background: unset;
@@ -257,7 +299,17 @@ ul.magicDelete > li.deleting {
background: transparent !important;
color: var(--text-color) !important;
}
#media-container {
width: 100%;
height: 100%;
}
#media-container video,
#media-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
#menu li,
#menu section {
margin: 8px auto !important;
@@ -288,7 +340,6 @@ ul.magicDelete > li.deleting {
#menu {
width: 270px;
z-index: 19;
transition-duration: 0.4s;
background: var(--better-main) !important;
color: var(--text-color);
border-right: none;
@@ -334,14 +385,16 @@ ul.magicDelete > li.deleting {
position: relative;
&::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 3px;
background: var(--item-colour, transparent);
transition: width 100ms, transform 0.3s ease;
transition:
width 100ms,
transform 0.3s ease;
border-radius: 8px 0 0 8px;
}
@@ -443,7 +496,7 @@ ol:has(.MessageList__avatar___2wxyb svg) {
.quickbar .actions [title="Choose a colour"] > svg {
scale: 0.9;
}
.quickbar[data-yiq='light'] .actions {
.quickbar[data-yiq="light"] .actions {
color: white !important;
}
.singleSelect > li {
@@ -470,7 +523,7 @@ ol:has(.MessageList__avatar___2wxyb svg) {
}
#main .timetablepage .quickbar {
border: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
border-radius: 16px;
}
.quickbar .actions {
@@ -501,7 +554,7 @@ ol:has(.MessageList__avatar___2wxyb svg) {
margin: 0 0 0 -12px;
background-color: rgba(255, 255, 255, 0.2);
clip-path: polygon(50% 40%, 0 0, 100% 0);
border: 12px solid rgba(255,255,255,0);
border: 12px solid rgba(255, 255, 255, 0);
border-top-color: transparent;
}
#main > .timetablepage > .quickbar.above::before {
@@ -1290,7 +1343,9 @@ div > ol:has(.uiFileHandlerWrapper) {
height: 25px;
width: 24px;
}
.notifications__notifications___3mmLY > button > .notifications__bubble___1EkSQ {
.notifications__notifications___3mmLY
> button
> .notifications__bubble___1EkSQ {
background: var(--better-alert-highlight);
width: 25px;
height: 25px;
@@ -1496,7 +1551,9 @@ iframe.userHTML {
.Collapsible__Collapsible___3O8P3 > .Collapsible__header___-Afvq {
background: none;
}
.AssessmentList__AssessmentList___1GdCl > .AssessmentList__searchFilter___3N70o + .AssessmentList__items___3LcmQ {
.AssessmentList__AssessmentList___1GdCl
> .AssessmentList__searchFilter___3N70o
+ .AssessmentList__items___3LcmQ {
color: var(--text-primary);
}
.Thermoscore__Thermoscore___2tWMi {
@@ -1546,19 +1603,10 @@ iframe.userHTML {
#main > .course > .content > .resources > h2 {
color: var(--text-primary);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
transition: 1s;
border-radius: 16px;
//width: 0px !important;
//background: none;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-corner {
background: none;
/* set button(top and bottom of the scrollbar) */
body::-webkit-scrollbar-button {
display: none !important;
}
:root,
html,
@@ -1567,8 +1615,19 @@ div,
ol,
ul {
scrollbar-width: thin !important;
scrollbar-color: var(--better-light) var(--better-sub);
scrollbar-color: #babac0 #fff !important;
}
.dark {
body,
div,
ol,
ul {
scrollbar-width: thin !important;
scrollbar-color: #333 #111 !important;
}
}
.connectedNotificationsWrapper > div > button {
color: var(--text-primary) !important;
height: 45px;
@@ -1592,10 +1651,13 @@ ul {
> .SelectedAssessment__clearBtn___21D85 {
background: var(--better-main);
}
.SelectedAssessment__SelectedAssessment___3Bu5D > .SelectedAssessment__meta___1gq_y {
.SelectedAssessment__SelectedAssessment___3Bu5D
> .SelectedAssessment__meta___1gq_y {
border-bottom: 1px solid var(--better-main);
}
.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk > li.TabSet__selected___1psfF {
.TabSet__TabSet___Vo-SZ
> ol.TabSet__tabs___1RRZk
> li.TabSet__selected___1psfF {
border-bottom-color: var(--better-main);
}
.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk {
@@ -1720,7 +1782,15 @@ div.entry.class[style*="left: 46.5%"] {
div.entry.class[style*="width: 46.5%"] {
width: 50% !important;
}
.timetablepage .dailycal > .content > .wrapper > .days > tbody > tr > td > .entriesWrapper {
.timetablepage
.dailycal
> .content
> .wrapper
> .days
> tbody
> tr
> td
> .entriesWrapper {
min-width: 0;
width: auto !important;
}
@@ -1914,7 +1984,6 @@ div.bar.flat {
.cke_toolbox > .cke_toolbar .cke_button_on {
background-color: #3d3d3e !important;
}
}
.legacy-root input.singleSelect:focus {
background: var(--auto-background);
@@ -1958,7 +2027,15 @@ body {
.forumView .assessment {
background: var(--better-main);
}
.dailycal > .content > .wrapper > .days > tbody > tr > td > .entriesWrapper > .entry {
.dailycal
> .content
> .wrapper
> .days
> tbody
> tr
> td
> .entriesWrapper
> .entry {
padding: 3px;
}
.Viewer__Viewer___32BH- {
@@ -1997,7 +2074,9 @@ li.MessageList__unread___3imtO {
border-radius: 1600px;
}
.MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz.MessageList__unread___3imtO {
.MessageList__MessageList___3DxoC
> ol
> li.MessageList__selected___1SJNz.MessageList__unread___3imtO {
box-shadow: none;
}
@@ -2017,7 +2096,9 @@ li.MessageList__unread___3imtO {
transition: width 0.1s;
}
.MessageList__MessageList___3DxoC > ol > li.MessageList__unread___3imtO::before {
.MessageList__MessageList___3DxoC
> ol
> li.MessageList__unread___3imtO::before {
width: 3px;
}
.connectedNotificationsWrapper > div > button {
@@ -2091,7 +2172,10 @@ li.MessageList__unread___3imtO {
cursor: pointer;
}
.dark .MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz {
.dark
.MessageList__MessageList___3DxoC
> ol
> li.MessageList__selected___1SJNz {
background: var(--background-secondary);
}
@@ -2509,20 +2593,21 @@ li.MessageList__unread___3imtO {
border-radius: 5px;
color: var(--text-color);
}
/* On mouse-over, add a grey background color */
.upcoming-checkbox-container:hover input ~ .upcoming-checkmark {
filter: brightness(0.8);
}
/* When the checkbox is checked, add a blue background */
.upcoming-checkbox-container input:checked ~ .upcoming-checkmark {
background: var(--item-colour);
}
/* Create the checkmark/indicator (hidden when not checked) */
.upcoming-checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
.upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after {
display: block;
@@ -2581,7 +2666,9 @@ li.MessageList__unread___3imtO {
width: 100%;
display: flex;
flex-direction: column;
box-shadow: inset 0px 6px 0 var(--item-colour, transparent), inset 0px 40px 50px -40px rgba(179, 179, 179, 0.9);
box-shadow:
inset 0px 6px 0 var(--item-colour, transparent),
inset 0px 40px 50px -40px rgba(179, 179, 179, 0.9);
transition: 200ms;
position: relative;
height: 15em;
@@ -2590,7 +2677,9 @@ li.MessageList__unread___3imtO {
font-family: Rubik, sans-serif;
}
.dark .day {
box-shadow: inset 0px 6px 0 var(--item-colour, transparent), inset 0px 40px 50px -40px rgba(0,0,0,0.9);
box-shadow:
inset 0px 6px 0 var(--item-colour, transparent),
inset 0px 40px 50px -40px rgba(0, 0, 0, 0.9);
}
.clickable {
cursor: pointer;
@@ -2627,7 +2716,7 @@ li.MessageList__unread___3imtO {
border-radius: 16px;
}
.dark .upcoming-items {
box-shadow: inset 0px 40px 80px -40px rgba(0,0,0,0.6);
box-shadow: inset 0px 40px 80px -40px rgba(0, 0, 0, 0.6);
}
.upcoming-assessment-title {
color: var(--text-primary);
@@ -2907,7 +2996,6 @@ li.MessageList__unread___3imtO {
width: 90%;
border-radius: 16px;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
}
.whatsnewTextContainer {
display: flex;
@@ -2977,9 +3065,20 @@ li.MessageList__unread___3imtO {
}
.whatsnewTextContainer h1:not(.whatsnewTextHeader) {
position: sticky;
font-size: 1.2em;
width: 100%;
top: 0;
background: var(--background-primary) !important;
z-index: 1;
padding: 10px;
padding: 12px;
padding-left: 0px;
padding-bottom: 8px;
}
.whatsnewTextContainer img {
width: 100%;
border-radius: 12px;
aspect-ratio: 16/9;
object-fit: cover;
margin-bottom: 12px;
}
+1
View File
@@ -36,4 +36,5 @@
transform-origin: 70% 0;
will-change: opacity, transform;
transform: translateZ(0); // promotes GPU rendering
transition: opacity 0.05s, transform 0.05s;
}
-51
View File
@@ -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
View File
@@ -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

-32
View File
@@ -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);
+7
View File
@@ -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>
-45
View File
@@ -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;
-11
View File
@@ -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 } = $props<{value: string, onChange: (value: string) => void}>()
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" bind:this={editor}></div>
-48
View File
@@ -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} />
)
}
+86
View File
@@ -0,0 +1,86 @@
div:has(> #rbgcp-wrapper) {
background: transparent !important;
}
.dark {
#rbgcp-wrapper {
div[style="padding-top: 11px; position: relative;"] div {
color: white !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,94 @@
<script lang="ts">
import { onMount } from 'svelte'
import ColourPicker from './ColourPicker.tsx';
import ReactAdapter from './utils/ReactAdapter.svelte';
import { animate, spring } 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] },
{ easing: spring({ stiffness: 400, damping: 30 }) }
);
animate(
background,
{ opacity: [1, 0] },
{ easing: [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, easing: [0.4, 0, 0.2, 1] }
);
animate(
content,
{ scale: [0.4, 1], opacity: [0, 1] },
{ easing: 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}
+108
View File
@@ -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={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;
+83
View File
@@ -0,0 +1,83 @@
<script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { animate as motionAnimate, spring } 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 dispatch = createEventDispatcher();
const playAnimation = (keyframe: any) => {
if (divElement && keyframe) {
let animationOptions = transition;
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`;
}
if (!transition || transition.type === 'spring') {
const springConfig = transition?.config || { stiffness: 250, damping: 25 };
animationOptions = {
...transition,
easing: spring(springConfig)
};
}
const animation = motionAnimate(divElement, finalKeyframe, animationOptions);
return animation.finished;
}
return Promise.resolve();
};
onMount(async () => {
if (initial) {
Object.assign(divElement.style, initial);
await playAnimation(animate || {});
} else if (animate) {
await playAnimation(animate);
}
dispatch('animationend');
});
$effect(() => {
if (animate) {
playAnimation(animate);
}
dispatch('animationend');
});
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>
-32
View File
@@ -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();
}
-127
View File
@@ -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,11 @@
<script lang="ts">
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
let { onClick } = $props<{ onClick: () => void }>();
</script>
<button
onclick={onClick}
style="background: {$settingsState.selectedColor}"
class="w-16 h-8 rounded-md"
></button>
-20
View File
@@ -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);
+22
View File
@@ -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>
-7
View File
@@ -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>
-19
View File
@@ -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%;
}
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
let { state, onChange } = $props<{ state: number, onChange: (value: number) => void }>();
let percentage = $derived((state / 100) * 100);
</script>
<div class="relative w-full max-w-lg mx-auto">
<input
type="range"
min="0"
max="100"
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>
-25
View File
@@ -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);
+34
View File
@@ -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>
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import { animate, spring } 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,
},
{
easing: spring(springParams),
}
);
};
// 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>
-35
View File
@@ -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,83 @@
<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 hoveredTab = $state<number | null>(null);
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;
};
$effect(() => {
calcXPos(hoveredTab);
});
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 bind:this={containerRef} class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 tab-width-container">
<div class="relative flex">
<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(hoveredTab) }}
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}
onmouseenter={() => hoveredTab = index}
onmouseleave={() => hoveredTab = null}
>
{title}
</button>
{/each}
</div>
</div>
<div class="h-full px-4 overflow-hidden">
<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);
-104
View File
@@ -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>;
};
-268
View File
@@ -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,322 @@
<script lang="ts">
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader';
import { setTheme } from '@/seqta/ui/themes/setTheme';
import Spinner from '../Spinner.svelte';
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import Fuse from 'fuse.js';
import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'
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>('');
// New state variables
let activeTab = $state<'all' | 'installed' | 'photos' | 'videos'>('all');
let sortBy = $state<'newest' | 'popular' | 'name'>('newest');
// Add Fuse.js options
const fuseOptions = {
keys: ['name', 'description'],
threshold: 0.4,
ignoreLocation: true
};
let fuse: Fuse<Background>;
// 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;
fuse = new Fuse(backgrounds, fuseOptions);
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 Fuse.js search if there's a search term
if (searchTerm.trim()) {
// @ts-ignore
if (fuse) {
filtered = fuse.search(searchTerm).map((result: any) => result.item) ?? [];
} else {
filtered = backgrounds.filter(bg => bg.name.toLowerCase().includes(searchTerm.toLowerCase()));
}
}
// 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;
setTheme('');
}
</script>
<div class="flex h-full">
<!-- Sidebar -->
<div class="w-64 h-full p-4 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="flex-1 overflow-auto">
<!-- 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 items-center justify-between mb-4">
<h1 class="text-2xl font-bold">Explore Backgrounds {searchTerm ? `- "${searchTerm}"` : ''}</h1>
<div class="flex items-center gap-4">
<select
bind:value={sortBy}
class="p-2 border rounded-lg 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-1 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="relative overflow-hidden 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 bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-zinc-300 dark:from-zinc-700 to-transparent">
<!-- Title placeholder -->
<div class="absolute bottom-2 left-2 right-2">
<div class="w-2/3 h-4 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>
<div class="w-1/2 h-3 mt-2 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="relative overflow-hidden 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="absolute inset-0 flex items-center justify-center transition-opacity duration-300 bg-black bg-opacity-50 opacity-0 group-hover: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">&#xed2c;</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">&#xea9a;</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 transition-opacity rounded-xl overflow-clip" 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 bg-gradient-to-t from-black/80 to-transparent'></div>
</div>
{/each}
</div>
</div>
<!-- Navigation buttons -->
<div class='absolute z-10 flex gap-2 bottom-2 right-2'>
<button aria-label="Previous" onclick={slidePrev} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
<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 items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
<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,63 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Background } from './types';
export let filteredBackgrounds: Background[];
let dispatch = createEventDispatcher();
let filters = $state({
type: [] as string[],
color: [] as string[],
resolution: [] as string[],
orientation: [] as string[]
});
$: {
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')} on:change={() => toggleFilter('type', 'image')}>
<span class="ml-2">Image</span>
</label>
<label class="flex items-center">
<input type="checkbox" checked={filters.type.includes('video')} on:change={() => toggleFilter('type', 'video')}>
<span class="ml-2">Video</span>
</label>
</div>
</div>
<!-- Add similar sections for color, resolution, and orientation -->
<button
class="px-4 py-2 mt-4 text-white bg-red-500 rounded hover:bg-red-600"
on:click={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 items-center justify-between px-4 py-1">
<div class="flex gap-4 cursor-pointer place-items-center" 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="relative flex 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 w-5 h-5 text-gray-400 transform -translate-y-1/2 left-3 top-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">&#xed8a;</span>
</button>
</div>
</div>
</header>
@@ -0,0 +1,17 @@
<script lang="ts">
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 z-10 mb-1 text-xl font-bold text-white bottom-1 left-3">
{theme.name}
</div>
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent'></div>
<div class='w-full'>
<img src={theme.coverImage} 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,118 @@
<script lang="ts">
import type { Theme } from '@/interface/types/Theme'
import { fade } from 'svelte/transition';
import { animate, spring } 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] },
{ easing: spring({ stiffness: 150, damping: 20 }) }
);
}
});
const hideModal = (relatedTheme?: Theme | null) => {
animate(
modalElement,
{ y: [10, 500], opacity: [1, 0] },
{ easing: spring({ stiffness: 150, damping: 20 }) }
);
setTimeout(() => {
setDisplayTheme(relatedTheme ?? null);
}, 100);
}
</script>
<div
class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-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 w-full mb-4 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="relative flex items-center justify-center w-32 px-4 py-2 mt-4 ml-auto 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="relative flex items-center justify-center w-32 px-4 py-2 mt-4 ml-auto 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 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
{relatedTheme.name}
</div>
<div class="absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent"></div>
<img src={relatedTheme.coverImage} alt="Theme Preview" class="object-cover w-full h-48" />
</div>
</button>
{/each}
</div>
</div>
</div>
</div>
-36
View File
@@ -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 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,210 @@
<script lang="ts">
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'
import { onDestroy, onMount } from 'svelte'
import { OpenThemeCreator } from '@/seqta/ui/ThemeCreator'
import shareTheme from '@/seqta/ui/themes/shareTheme'
import { InstallTheme } from '@/seqta/ui/themes/downloadTheme'
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
import { setTheme } from '@/seqta/ui/themes/setTheme'
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
import { OpenStorePage } from '@/seqta/ui/renderStore'
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
import { closeExtensionPopup } from '@/SEQTA'
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 disableTheme();
themes.selectedTheme = '';
} else {
await setTheme(theme.id);
if (!themes) return;
themes.selectedTheme = theme.id;
}
}
const handleThemeDelete = async (themeId: string) => {
try {
await deleteTheme(themeId);
if (!themes) return;
themes.themes = themes.themes.filter(theme => theme.id !== themeId);
if (themeId === themes.selectedTheme) {
themes.selectedTheme = '';
await disableTheme();
}
} catch (error) {
console.error('Error deleting theme:', error);
}
}
const handleShareTheme = async (theme: CustomTheme) => {
try {
await 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 InstallTheme(result);
await fetchThemes();
} catch (error) {
alert('Error parsing file. Please upload a valid JSON theme file.');
}
tempTheme = null;
};
reader.readAsText(file);
}
const fetchThemes = async () => {
themes = await getAvailableThemes();
}
onMount(async () => {
await fetchThemes();
themeUpdates.addListener(fetchThemes);
})
onDestroy(() => {
themeUpdates.removeListener(fetchThemes);
})
</script>
<div
class="w-full pt-5 mb-1"
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 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 class="flex items-center justify-center h-full">
<div class="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 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="absolute z-20 flex w-6 h-6 p-2 text-white bg-red-600 rounded-full opacity-100 right-2 place-items-center top-2"
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">&#xeaa5;</span>
</div>
<div
class="absolute z-20 flex w-8 h-8 p-2 text-center transition-all -translate-y-1/2 rounded-full opacity-0 text-white/80 top-1/4 right-12 bg-black/50 place-items-center 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">&#xecb3;</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="absolute inset-0 z-0 object-cover 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 w-full bg-gray-200 rounded-xl dark:bg-zinc-700/50 place-items-center aspect-theme animate-pulse">
<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 items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
>
<span class="text-xl font-IconFamily">&#xecc5;</span>
<span class="ml-2">Theme Store</span>
</button>
<button
onclick={() => { OpenThemeCreator(); closeExtensionPopup() }}
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
>
<span class="text-xl font-IconFamily">&#xec60;</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>
-8
View File
@@ -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);
});
};
+29
View File
@@ -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();
+37
View File
@@ -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();
-161
View File
@@ -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,
});
};
+29
View File
@@ -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);
-100
View File
@@ -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;
+35
View File
@@ -1,3 +1,5 @@
@import './components/ColourPicker.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -15,3 +17,36 @@
::-webkit-scrollbar {
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;
max-height: 400px;
}
+3 -5
View File
@@ -5,10 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BetterSEQTA+ Settings</title>
</head>
<body class="dark:bg-zinc-900">
<div id="ExtensionPopup"></div>
<script type="module" src="./main.tsx"></script>
<script type="module" src="./dark.ts"></script>
<body>
<div id="app"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
import "./index.css"
import { mount } from "svelte"
import type { ComponentType } from "svelte"
import Settings from "./pages/settings.svelte"
import IconFamily from '@/resources/fonts/IconFamily.woff'
import browser from "webextension-polyfill"
export default function renderSvelte(
Component: ComponentType | any,
mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {},
) {
const app = mount(Component, {
target: mountPoint,
props: {
standalone: true,
...props,
},
})
return app
}
function InjectCustomIcons() {
console.info('[BetterSEQTA+] Injecting Icons')
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = `
@font-face {
font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal;
font-style: normal;
}`
document.head.appendChild(style)
}
const mountPoint = document.getElementById('app')
if (!mountPoint) {
console.error('Mount point #app not found')
throw new Error('Mount point #app not found')
}
InjectCustomIcons()
renderSvelte(Settings, mountPoint)
-2
View File
@@ -4,5 +4,3 @@ declare module "*.png";
declare module "*.svg";
declare module "*.jpeg";
declare module "*.jpg";
declare module 'react-best-gradient-color-picker';
+24
View File
@@ -0,0 +1,24 @@
import styles from "./index.css?inline"
import { mount } from "svelte"
import type { ComponentType } from "svelte"
export default function renderSvelte(
Component: ComponentType | any,
mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {},
) {
const app = mount(Component, {
target: mountPoint,
props: {
standalone: false,
...props,
},
})
const style = document.createElement("style")
style.setAttribute("type", "text/css")
style.innerHTML = styles
mountPoint.appendChild(style)
return app
}
-59
View File
@@ -1,59 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Route, Routes } from 'react-router-dom';
import { ErrorBoundary } from "react-error-boundary";
import './index.css';
import SettingsPage from './pages/SettingsPage.js';
import browser from 'webextension-polyfill';
import font from '../resources/fonts/IconFamily.woff'
import ThemeCreator from './pages/ThemeCreator';
import Store from './pages/Store';
browser.storage.local.get().then(({ DarkMode }) => {
if (DarkMode) document.documentElement.classList.add('dark');
})
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.classList.add('iconFamily')
style.innerHTML = `
@font-face {
font-family: 'IconFamily';
src: url('${browser.runtime.getURL(font)}') format('woff');
font-weight: normal;
font-style: normal;
}`;
document.head.appendChild(style);
const root = ReactDOM.createRoot(document.getElementById('ExtensionPopup')!);
root.render(
<React.StrictMode>
<ErrorBoundary fallback={
<div className="grid w-full h-screen text-center place-content-center dark:text-white">
<h1 className="text-2xl font-bold">An error occurred 😭😭😭</h1>
<p className="text-lg">Try clicking this button and see if it helps...</p>
<button className='flex gap-2 p-2 px-4 mx-auto mt-4 rounded-lg dark:text-white bg-zinc-100 dark:bg-zinc-800/20 outline outline-white/20 w-fit' onClick={() => window.location.reload()}>
<svg height="18" width="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path d="M9.03,12.22c-.293-.293-.768-.293-1.061,0s-.293,.768,0,1.061l1.208,1.208c-.059,.002-.118,.012-.178,.012-3.032,0-5.5-2.467-5.5-5.5,0-1.616,.706-3.143,1.938-4.191,.315-.269,.354-.742,.085-1.057s-.74-.353-1.058-.085c-1.567,1.333-2.466,3.277-2.466,5.333,0,3.76,2.983,6.829,6.704,6.985l-.735,.735c-.293,.293-.293,.768,0,1.061,.146,.146,.338,.22,.53,.22s.384-.073,.53-.22l2.25-2.25c.293-.293,.293-.768,0-1.061l-2.25-2.25Z" fill="currentColor"/>
<path d="M9.296,2.015l.735-.735c.293-.293,.293-.768,0-1.061s-.768-.293-1.061,0l-2.25,2.25c-.293,.293-.293,.768,0,1.061l2.25,2.25c.146,.146,.338,.22,.53,.22s.384-.073,.53-.22c.293-.293,.293-.768,0-1.061l-1.208-1.208c.059-.002,.118-.012,.177-.012,3.032,0,5.5,2.467,5.5,5.5,0,1.616-.706,3.143-1.938,4.191-.315,.269-.354,.742-.085,1.057,.148,.174,.359,.264,.571,.264,.172,0,.345-.059,.486-.179,1.567-1.333,2.466-3.277,2.466-5.333,0-3.76-2.983-6.829-6.704-6.985Z" fill="currentColor"/>
</g>
</svg>
Reload
</button>
</div>
}>
<HashRouter>
<Routes>
<Route path="/settings" element={<SettingsPage standalone={true} />} />
<Route path="/settings/embedded" element={<SettingsPage standalone={false} />} />
<Route path="/store" element={<Store />} />
<Route path="/themeCreator" element={<ThemeCreator />} />
</Routes>
</HashRouter>
</ErrorBoundary>
</React.StrictMode>,
);
-50
View File
@@ -1,50 +0,0 @@
import TabbedContainer from '../components/TabbedContainer';
import Settings from './SettingsPage/Settings';
import logo from '../../resources/icons/betterseqta-dark-full.png';
import logoDark from '../../resources/icons/betterseqta-light-full.png';
import { SettingsContextProvider } from '../SettingsContext';
import Shortcuts from './SettingsPage/Shortcuts';
import Picker from '../components/Picker';
import Themes from './SettingsPage/Themes';
import { ToastContainer } from 'react-toastify';
import { memo } from 'react';
import browser from 'webextension-polyfill';
interface SettingsPage {
standalone: boolean;
}
const SettingsPage = ({ standalone }: SettingsPage) => {
const tabs = [
{
title: 'Settings',
content: <Settings />
},
{
title: 'Shortcuts',
content: <Shortcuts />
},
{
title: 'Themes',
content: <Themes />
}
];
return (
<SettingsContextProvider>
<ToastContainer stacked toastStyle={{ borderRadius: '16px' }} draggable theme={document.body.classList.contains('dark') ? 'dark' : 'light'} />
<div className={`flex flex-col w-[384px] shadow-2xl gap-2 bg-white ${ standalone ? 'h-[600px]' : 'h-[100vh] rounded-xl' } overflow-clip dark:bg-zinc-800 dark:text-white`}>
<div className="grid border-b border-b-zinc-200/40 place-items-center">
<img src={logo} className="w-4/5 dark:hidden" />
<img src={logoDark} className="hidden w-4/5 dark:block" />
<button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenChangelog' })} className="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-1 bg-zinc-100 dark:bg-zinc-700"></button>
</div>
<Picker />
<TabbedContainer tabs={tabs} animations={false} />
</div>
</SettingsContextProvider>
);
};
export default memo(SettingsPage);
@@ -1,19 +0,0 @@
const About = () => {
return (
<div className="flex flex-col overflow-y-scroll divide-y divide-zinc-100/50 dark:divide-zinc-700/50">
<div>
<h2 className="text-lg font-bold">About</h2>
<p className="py-2">BetterSEQTA+ is a branch of BetterSEQTA which was originally developed by Nulkem. It was discontinued. So BetterSEQTA+ has come in to fill in that gap!</p>
<p className="py-2">We are currently working on fixing bugs and adding new features. If you want to request a feature or report a bug, you can do so on
<a className="pl-1 text-blue-500 underline hover:text-blue-600" href="https://github.com/BetterSEQTA/BetterSEQTA-plus" target="_blank">Github</a>.
</p>
</div>
<div>
<h2 className="pt-2 text-lg font-bold">Credits</h2>
<p className="py-2">Nulkem for the original extension, OG-RandomTechChannel, Crazypersonalph, and the current maintainer SethBurkart123</p>
</div>
</div>
);
};
export default About;
@@ -1,160 +0,0 @@
import Switch from '../../components/Switch';
import Slider from '../../components/Slider';
import PickerSwatch from '../../components/PickerSwatch';
import Select from '../../components/Select';
import { SettingsList } from '../../types/SettingsProps';
import { useSettingsContext } from '../../SettingsContext';
import browser from 'webextension-polyfill';
import { memo, useCallback } from 'react';
const Settings: React.FC = () => {
const { settingsState, setSettingsState } = useSettingsContext();
const handleDevModeToggle = useCallback(() => {
const secretSequence = 'dev';
let typedSequence = '';
let timeoutId: NodeJS.Timeout;
const handleKeyDown = (event: KeyboardEvent) => {
typedSequence += event.key.toLowerCase();
if (typedSequence.includes(secretSequence)) {
document.removeEventListener('keydown', handleKeyDown);
clearTimeout(timeoutId);
setSettingsState(prevState => ({
...prevState,
devMode: !prevState.devMode
}));
alert(`Dev mode is now ${!settingsState.devMode ? 'enabled' : 'disabled'}`);
}
// Clear the sequence after 2 seconds of inactivity
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
typedSequence = '';
}, 2000);
};
document.addEventListener('keydown', handleKeyDown);
// Cleanup function to remove the event listener
return () => {
document.removeEventListener('keydown', handleKeyDown);
clearTimeout(timeoutId);
};
}, [setSettingsState, settingsState.devMode]);
const handleSettingChange = useCallback((key: string, value: boolean | string | number) => {
setSettingsState(prevState => ({
...prevState,
[key]: value,
}));
}, [setSettingsState]);
const settings: SettingsList[] = [
{
title: "Transparency Effects",
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
modifyElement: <Switch state={settingsState.transparencyEffects} onChange={(isOn: boolean) => handleSettingChange('transparencyEffects', isOn)} />
},
{
title: "Animated Background",
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
modifyElement: <Switch state={settingsState.animatedBackground} onChange={(isOn: boolean) => handleSettingChange('animatedBackground', isOn)} />
},
{
title: "Animated Background Speed",
description: "Controls the speed of the animated background.",
modifyElement: <Slider state={parseInt(settingsState.animatedBackgroundSpeed)} onChange={(value: number) => handleSettingChange('animatedBackgroundSpeed', value)} />
},
{
title: "Custom Theme Colour",
description: "Customise the overall theme colour of SEQTA Learn.",
modifyElement: <PickerSwatch />
},
{
title: "Edit Sidebar Layout",
description: "Customise the sidebar layout.",
modifyElement: <button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' })} className='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>Edit</button>
},
{
title: "Animations",
description: "Enables animations on certain pages.",
modifyElement: <Switch state={settingsState.animations} onChange={(isOn: boolean) => handleSettingChange('animations', isOn)} />
},
{
title: "Notification Collector",
description: "Uncaps the 9+ limit for notifications, showing the real number.",
modifyElement: <Switch state={settingsState.notificationCollector} onChange={(isOn: boolean) => handleSettingChange('notificationCollector', isOn)} />
},
{
title: "Lesson Alerts",
description: "Sends a native browser notification ~5 minutes prior to lessons.",
modifyElement: <Switch state={settingsState.lessonAlerts} onChange={(isOn: boolean) => handleSettingChange('lessonAlerts', isOn)} />
},
{
title: "12 Hour Time",
description: "Prefer 12 hour time format for SEQTA",
modifyElement: <Switch state={settingsState.timeFormat == "12"} onChange={(isOn: boolean) => handleSettingChange('timeFormat', isOn ? "12" : "24")} />
},
{
title: "Default Page",
description: "The page to load when SEQTA Learn is opened.",
modifyElement: <Select state={settingsState.defaultPage} onChange={(value: string) => handleSettingChange('defaultPage', value)} options={[
{ value: 'home', label: 'Home' },
{ value: 'dashboard', label: 'Dashboard' },
{ value: 'timetable', label: 'Timetable' },
{ value: 'welcome', label: 'Welcome' },
{ value: 'messages', label: 'Messages' },
{ value: 'documents', label: 'Documents' },
{ value: 'reports', label: 'Reports' },
]} />
},
{
title: "BetterSEQTA+",
description: "Enables BetterSEQTA+ features",
modifyElement: <Switch state={settingsState.betterSEQTAPlus} onChange={(isOn: boolean) => handleSettingChange('betterSEQTAPlus', isOn)} />
}
];
return (
<div className="flex flex-col -mt-4 overflow-y-scroll divide-y divide-zinc-100 dark:divide-zinc-700">
{settings.map((setting, index) => (
<div className="flex items-center justify-between px-4 py-3" key={index}>
<div className="pr-4">
<h2 onClick={setting.title.includes('BetterSEQTA+') ? handleDevModeToggle : undefined} className="text-sm font-bold">{setting.title}</h2>
<p className="text-xs">{setting.description}</p>
</div>
<div>
{setting.modifyElement}
</div>
</div>
))}
{settingsState.devMode && (
<>
<div className="flex items-center justify-between px-4 py-3">
<div className="pr-4">
<h2 className="text-sm font-bold">Dev Mode</h2>
<p className="text-xs">Enables dev mode</p>
</div>
<Switch state={settingsState.devMode} onChange={(isOn: boolean) => handleSettingChange('devMode', isOn)} />
</div>
<div className="flex items-center justify-between px-4 py-3">
<div className="pr-4">
<h2 className="text-sm font-bold">Sensitive Hider</h2>
<p className="text-xs">Replace sensitive content with mock data</p>
</div>
<button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'HideSensitive' })} className='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>Hide</button>
</div>
</>
)}
</div>
);
};
export default memo(Settings);
@@ -1,147 +0,0 @@
import { memo, useCallback, useState } from "react";
import Switch from "../../components/Switch";
import { useSettingsContext } from "../../SettingsContext";
import { AnimatePresence, motion } from "framer-motion";
import { CustomShortcut } from "../../types/AppProps";
function formatUrl(inputUrl: string) {
const protocolRegex = /^(http:\/\/|https:\/\/|ftp:\/\/)/;
return protocolRegex.test(inputUrl) ? inputUrl : `https://${inputUrl}`;
}
const Shortcuts = memo(() => {
const { settingsState, setSettingsState } = useSettingsContext();
const [newTitle, setNewTitle] = useState<string>("");
const [isFormVisible, setFormVisible] = useState(false);
const [newURL, setNewURL] = useState<string>("");
const switchChange = useCallback((shortcutName: string, isOn: boolean) => {
setSettingsState((prevState) => {
const updatedShortcuts = prevState.shortcuts.map((shortcut) =>
shortcut.name === shortcutName ? { ...shortcut, enabled: isOn } : shortcut
);
return { ...prevState, shortcuts: updatedShortcuts };
});
}, [setSettingsState]);
const isValidTitle = useCallback((title: string) => title.trim() !== "", []);
const isValidURL = useCallback((url: string) => {
const pattern = new RegExp("^(https?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\-]+)*(?::\\d+)?(/[\\w\\-./]*)*$", "i");
return pattern.test(url);
}, []);
const addNewCustomShortcut = useCallback(() => {
if (isValidTitle(newTitle) && isValidURL(newURL)) {
const newShortcut: CustomShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon: newTitle[0] };
const updatedCustomShortcuts = [...settingsState.customshortcuts, newShortcut];
setSettingsState({ ...settingsState, customshortcuts: updatedCustomShortcuts });
setNewTitle("");
setNewURL("");
setFormVisible(false);
} else {
// Replace with a more user-friendly way to display errors
alert("Please enter a valid title and URL.");
}
}, [newTitle, newURL, isValidTitle, isValidURL, setSettingsState]);
const deleteCustomShortcut = useCallback((index: number) => {
setSettingsState((prevState) => ({
...prevState,
customshortcuts: prevState.customshortcuts.filter((_, i) => i !== index),
}));
}, [setSettingsState]);
const toggleForm = useCallback(() => {
setFormVisible((isVisible) => !isVisible);
}, []);
return (
<div className="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
<AnimatePresence>
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={isFormVisible ? { opacity: 1, height: "auto" } : { opacity: 0, height: 0 }}
exit={{ opacity: 0, height: 0 }}
transition={{ type: "spring", damping: 20 }}
>
{isFormVisible &&
<div className="flex flex-col items-center mb-4">
<motion.input
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="w-full p-2 rounded-md bg-zinc-100 dark:bg-zinc-700 focus:outline-none"
type="text"
placeholder="Shortcut Name"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
<motion.input
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="w-full p-2 my-2 rounded-md bg-zinc-100 dark:bg-zinc-700 focus:outline-none"
type="text"
placeholder="URL eg. https://google.com"
value={newURL}
onChange={(e) => setNewURL(e.target.value)}
/>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="w-full px-4 py-2 text-white bg-blue-500 rounded-md"
onClick={ addNewCustomShortcut }
>
Add
</motion.button>
</div>
}
</motion.div>
</AnimatePresence>
{!isFormVisible && (
<button
className="w-full px-4 py-2 mb-4 text-white bg-blue-500 rounded"
onClick={toggleForm}
>
Add Custom Shortcut
</button>
)}
{/* Shortcuts Section */}
{settingsState.shortcuts ? (
settingsState.shortcuts.map((shortcut, index) => shortcut.name && (
<div className="flex items-center justify-between px-4 py-3" key={index}>
{shortcut.name}
<Switch state={shortcut.enabled} onChange={(isOn) => switchChange(shortcut.name, isOn)} />
</div>
))
) : (
<p>Loading shortcuts...</p>
)}
{/* Custom Shortcuts Section */}
{settingsState.customshortcuts ? (
settingsState.customshortcuts.map((shortcut, index) => (
<div className="flex items-center justify-between px-4 py-3" key={index}>
{shortcut.name}
<button onClick={() => deleteCustomShortcut(index)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))
) : (
<p>Loading custom shortcuts...</p>
)}
</div>
);
});
export default Shortcuts;
@@ -1,30 +0,0 @@
import { createRef, FC, useState } from 'react';
import BackgroundSelector from '../../components/BackgroundSelector';
import ThemeSelector from '../../components/ThemeSelector';
import { memo } from 'react';
type ThemeSelectorRef = {
disableTheme: () => void;
};
const Themes: FC = () => {
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const themeSelectorRef = createRef<ThemeSelectorRef>();
const disableTheme = async () => {
themeSelectorRef?.current?.disableTheme();
}
return (
<div className="">
<button className="absolute top-12 z-20 right-0 p-2 text-[0.8rem] text-blue-500" onClick={() => setIsEditMode(!isEditMode)}>
{isEditMode ? 'Done' : 'Edit'}
</button>
<BackgroundSelector disableTheme={disableTheme} isEditMode={isEditMode} />
<ThemeSelector ref={themeSelectorRef} isEditMode={isEditMode} />
</div>
);
};
export default memo(Themes);
-335
View File
@@ -1,335 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import Header from '../components/store/header';
import { Autoplay } from 'swiper/modules';
import { AnimatePresence, motion } from 'framer-motion';
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/scrollbar';
import 'swiper/css/autoplay';
import SpinnerIcon from '../components/LoadingSpinner';
import localforage from 'localforage';
import { StoreDownloadTheme } from '../../seqta/ui/themes/downloadTheme';
const textVariants = {
hidden: { opacity: 0, y: 60 },
visible: { opacity: 1, y: 0, transition: {
type: 'spring',
bounce: 0,
stiffness: 80,
damping: 12
} },
};
const containerVariants = {
hidden: {
y: '100vh',
},
visible: {
y: 0,
transition: {
staggerChildren: 0.05,
type: 'spring',
bounce: 0,
stiffness: 400,
damping: 50
},
},
};
export type Theme = {
name: string;
description: string;
coverImage: string;
marqueeImage: string;
id: string;
}
type ThemesResponse = {
themes: Theme[];
}
export const DeleteDownloadedTheme = async (themeID: string) => {
console.debug('DeleteDownloaded Theme:', themeID)
await localforage.removeItem(themeID);
const availableThemesList = await localforage.getItem('availableThemes') as string[];
const updatedThemesList = availableThemesList.filter(theme => theme !== themeID);
await localforage.setItem('availableThemes', updatedThemesList);
}
const Store = () => {
const [searchTerm, setSearchTerm] = useState('');
const swiperCover = useRef<any | null>(null);
const [gridThemes, setGridThemes] = useState<Theme[]>([]);
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
const [coverThemes, setCoverThemes] = useState<Theme[]>([]);
const [installingThemes, setInstallingThemes] = useState<string[]>([]);
const [currentThemes, setCurrentThemes] = useState<string[]>([]);
const [displayTheme, setDisplayTheme] = useState<Theme | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const fetchThemes = async () => {
try {
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${(new Date()).getTime()}`, { cache: 'no-store' });
const data: ThemesResponse = await response.json();
setGridThemes(data.themes);
// Select up to 3 random themes to display in coverThemes
const shuffled = [...data.themes].sort(() => 0.5 - Math.random());
setCoverThemes(shuffled.slice(0, 3));
setLoading(false);
} catch (error) {
console.error('Failed to fetch themes', error);
// Retry after 5 seconds
setTimeout(fetchThemes, 5000);
}
};
useEffect(() => {
(async () => {
document.title = 'BetterSEQTA+ Store';
fetchThemes();
const availableThemes = await localforage.getItem('availableThemes') as string[] | null;
if (availableThemes) {
setCurrentThemes(availableThemes)
}
})();
}, []);
useEffect(() => {
setFilteredThemes(gridThemes.filter(theme =>
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) || theme.description.toLowerCase().includes(searchTerm.toLowerCase())
));
}, [searchTerm, gridThemes]);
const downloadTheme = (id: string) => {
const themeContent = gridThemes.find(theme => theme.id === id);
if (!themeContent) {
alert('There was an error, The theme was not found!')
return
}
setInstallingThemes([...installingThemes, id]);
StoreDownloadTheme({ themeContent }).then(() => {
setInstallingThemes(installingThemes.filter(theme => theme !== id));
setCurrentThemes([...currentThemes, id]);
});
};
const removeTheme = async (id: string) => {
const themeContent = gridThemes.find(theme => theme.id === id);
if (!themeContent) {
alert('There was an error, The theme was not found!')
return
}
setInstallingThemes([...installingThemes, id]);
DeleteDownloadedTheme(id).then(() => {
setInstallingThemes(installingThemes.filter(theme => theme !== id));
setCurrentThemes(currentThemes.filter(theme => theme !== id));
});
}
return (
<div className="w-screen h-screen overflow-y-scroll pt-[4.25rem] bg-zinc-200/50 dark:bg-zinc-900 dark:text-white">
<Header searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
{/* loader */}
<div className={`flex items-center justify-center w-full h-full ${!loading && 'hidden'}`}>
<SpinnerIcon className="w-16 h-16" />
</div>
<div className={`px-24 py-12 ${loading && 'hidden'}`}>
<div className={`relative w-full rounded-xl overflow-clip transition-opacity ${searchTerm == '' ? 'opacity-100' : 'opacity-0'}`}>
<motion.div className='overflow-clip' animate={{
height: searchTerm == '' ? 'auto' : '0px'
}} transition={{
type: 'spring',
bounce: 0,
duration: 1,
stiffness: 200,
damping: 30
}}>
<Swiper
ref={swiperCover}
spaceBetween={20}
slidesPerView={1}
loop={true}
className='w-full aspect-[8/3]'
modules={[Autoplay]}
autoplay={{
delay: 5000,
stopOnLastSlide: false,
disableOnInteraction: false,
pauseOnMouseEnter: true
}}
>
{ [...coverThemes, ...coverThemes].map((theme, index) => (
<SwiperSlide className='relative cursor-pointer rounded-xl overflow-clip' onClick={() => setDisplayTheme(theme)} key={index}>
<img
src={theme.marqueeImage}
alt="Theme Preview"
className="object-cover w-full h-full"
/>
<div className='absolute bottom-0 left-0 p-8 z-[1]'>
<h2 className='text-4xl font-bold text-white'>{theme.name}</h2>
<p className='text-lg text-white'>{theme.description}</p>
</div>
{/* shadow from the bottom of the image */}
<div className='absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-black/80 to-transparent' />
</SwiperSlide>
)) }
</Swiper>
</motion.div>
<div className={displayTheme ? 'pointer-events-auto' : 'pointer-events-none'}>
<AnimatePresence>
{displayTheme && (
<motion.div
className={`fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-70`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setDisplayTheme(null)}
>
<motion.div
onClick={(e) => e.stopPropagation()}
className="w-full max-w-xl h-[95%] p-4 bg-white rounded-t-2xl dark:bg-zinc-800 overflow-scroll"
exit={{ y: "100vh" }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
variants={containerVariants}
initial="hidden"
animate="visible"
>
<motion.div className="relative h-auto">
<motion.button
className="absolute top-0 right-0 p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200"
onClick={() => setDisplayTheme(null)}
variants={textVariants}
>
{'\ued8a'}
</motion.button>
<motion.h2 className="mb-4 text-2xl font-bold" variants={textVariants}>
{displayTheme.name}
</motion.h2>
<motion.img src={displayTheme.marqueeImage} alt="Theme Cover" className="object-cover w-full mb-4 rounded-md" variants={textVariants} />
<motion.p className="mb-4 text-gray-700 dark:text-gray-300" variants={textVariants}>
{displayTheme.description}
</motion.p>
{
currentThemes.includes(displayTheme.id) ?
<motion.button
variants={textVariants}
onClick={() => removeTheme(displayTheme.id)}
className="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200 focus:outline-none focus:ring-2 focus:ring-zinc-800 focus:ring-offset-2">
{ installingThemes.includes(displayTheme.id) ?
<>
<SpinnerIcon className="w-4 h-4 mr-2" />
Removing...
</> :
<> Remove </>
}
</motion.button> :
<motion.button
variants={textVariants}
onClick={() => downloadTheme(displayTheme.id)}
className="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200 focus:outline-none focus:ring-2 focus:ring-zinc-800 focus:ring-offset-2">
{ installingThemes.includes(displayTheme.id) ?
<>
<SpinnerIcon className="w-4 h-4 mr-2" />
Installing...
</> :
<> Install </>
}
</motion.button>
}
<motion.div className="my-8 border-b border-zinc-200 dark:border-zinc-700" variants={textVariants} />
<motion.h3 className="mb-4 text-lg font-bold" variants={textVariants}>
Similar Themes
</motion.h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{gridThemes.filter(theme => theme.id !== displayTheme.id).sort((a, b) => a.name.localeCompare(displayTheme.name) - b.name.localeCompare(displayTheme.name)).map((theme, index) => (
<motion.div key={index} onClick={() => { setDisplayTheme(null); setDisplayTheme(theme); }} className='w-full cursor-pointer' variants={textVariants}>
<div className="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 className="absolute z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
{theme.name}
</div>
<div className='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent' />
<img src={theme.coverImage} alt="Theme Preview" className="object-cover w-full h-48" />
</div>
</motion.div>
))}
</div>
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* pagination */}
<div className='absolute z-10 flex gap-2 bottom-2 right-2'>
<button onClick={ () => {swiperCover.current?.swiper.slidePrev() }} className='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
</svg>
</button>
<button onClick={ () => {swiperCover.current?.swiper.slideNext() }} className='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
{filteredThemes.map((theme, index) => (
<div onClick={() => setDisplayTheme(theme)} key={index} className='w-full cursor-pointer'>
<div className="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 className="absolute z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
{theme.name}
</div>
<div className='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent' />
<div
className='w-full'>
<img src={theme.coverImage} alt="Theme Preview" className="object-cover w-full h-48 rounded-md" />
</div>
</div>
</div>
))}
<a href="https://betterseqta.gitbook.io/betterseqta-docs" className='w-full cursor-pointer'>
<div className="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 className="text-2xl font-IconFamily">{'\uecb3'}</div>
<div className="text-xl font-bold text-center transition-all duration-500 dark:text-white">
Got a Theme Idea?
<p className="text-lg font-light subtitle">Transform it into a stunning theme!</p>
</div>
</div>
</a>
</div>
{filteredThemes.length == 0 && !loading && (
<div className="flex flex-col items-center justify-center w-full text-center h-96">
<h1 className="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">That doesnt exist! 😭😭😭</h1>
<p className="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, we couldnt find the theme youre looking for. Maybe... you could create it?</p>
</div>
)}
</div>
</div>
);
};
export default Store;
-341
View File
@@ -1,341 +0,0 @@
import CodeEditor from '../components/CodeEditor';
import { useEffect, useState } from 'react';
import ColorPicker from 'react-best-gradient-color-picker';
import Accordion from '../components/Accordian';
import Switch from '../components/Switch';
import { sendThemeUpdate } from '../hooks/ThemeManagment';
import { MoonIcon, PlusIcon, SunIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { v4 as uuidv4 } from 'uuid';
import { CustomTheme, CustomThemeBase64 } from '../types/CustomThemes';
import browser from 'webextension-polyfill';
function ThemeCreator() {
// default settings for new themes
const [theme, setTheme] = useState<CustomTheme>({
id: uuidv4(),
name: '',
description: '',
defaultColour: '',
CanChangeColour: true,
allowBackgrounds: true,
CustomCSS: '',
CustomImages: [],
coverImage: null,
isEditable: true,
hideThemeName: false,
forceDark: undefined,
});
useEffect(() => {
const getTheme = async (themeID: string) => {
const theme = await browser.runtime.sendMessage({
type: 'currentTab',
info: 'GetTheme',
body: {
themeID: themeID,
}
}) as CustomThemeBase64 | undefined;
if (theme) {
// base64toblob to convert it to a blob url
const CustomImages = theme.CustomImages.map((image) => {
const base64Index = image.url.indexOf(',') + 1;
const imageBase64 = image.url.substring(base64Index);
// Convert base64 to blob
const byteCharacters = atob(imageBase64);
const byteNumbers = new Uint8Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'image/png' });
return {
id: image.id,
blob: blob,
variableName: image.variableName,
};
});
const coverImageBase64 = theme.coverImage;
let coverImageBlob = null;
if (coverImageBase64) {
const base64Index = coverImageBase64.indexOf(',') + 1;
const imageBase64 = coverImageBase64.substring(base64Index);
// Convert base64 to blob
const byteCharacters = atob(imageBase64);
const byteNumbers = new Uint8Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
coverImageBlob = new Blob([byteArray], { type: 'image/png' });
}
setTheme({
...theme,
CustomImages,
coverImage: coverImageBlob
});
sendThemeUpdate({
...theme,
CustomImages: CustomImages,
}, false, true);
}
};
// get ThemeID from URL params
const urlParams = new URLSearchParams(window.location.search);
const themeID = urlParams.get('themeID');
if (themeID) {
getTheme(themeID);
}
}, []);
const generateImageId = () => {
return Math.random().toString(36).substr(2, 9);
};
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = '';
if (file) {
const reader = new FileReader();
reader.onload = async () => {
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
const imageId = generateImageId();
const variableName = `custom-image-${theme.CustomImages.length}`;
const updatedTheme = {
...theme,
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName }],
};
setTheme(updatedTheme);
sendThemeUpdate(updatedTheme, false, true);
};
reader.readAsDataURL(file);
}
};
const handleRemoveImage = (imageId: string) => {
const updatedTheme = {
...theme,
CustomImages: theme.CustomImages.filter((image) => image.id !== imageId),
};
setTheme(updatedTheme);
};
const handleImageVariableChange = (imageId: string, variableName: string) => {
const updatedTheme = {
...theme,
CustomImages: theme.CustomImages.map((image) =>
image.id === imageId ? { ...image, variableName } : image
),
};
setTheme(updatedTheme);
};
function CodeUpdate(value: string) {
setTheme((prevTheme) => ({
...prevTheme,
CustomCSS: value,
}));
}
const saveTheme = async () => {
sendThemeUpdate(theme, true)
};
useEffect(() => {
sendThemeUpdate(theme);
}, [theme]);
return (
<div className='w-full min-h-[100vh] bg-zinc-100 dark:bg-zinc-800 dark:text-white transition duration-30'>
<div className='flex flex-col p-2'>
<h1 className='text-xl font-semibold'>Theme Creator</h1>
<a href="https://betterseqta.gitbook.io/betterseqta-docs" target="_blank" className='text-sm font-light text-zinc-500 dark:text-zinc-400'>
<span className="no-underline font-IconFamily pr-0.5">{'\ueb44'}</span>
<span className="underline">
Need help? Check out the docs!
</span>
</a>
<Divider />
<div>
<div className='pb-2 text-sm'>Theme Name</div>
<input
id='themeName'
type='text'
placeholder='What is your theme called?'
value={theme.name}
onChange={e => setTheme({ ...theme, name: e.target.value })}
className='w-full p-2 mb-4 transition-all duration-300 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-900 dark:text-white' />
</div>
<div>
<div className='pb-2 text-sm'>Description <span className='italic font-light opacity-80'>(optional)</span></div>
<textarea
id='themeDescription'
placeholder="Don't worry, this one's optional!"
value={theme.description}
onChange={e => setTheme({ ...theme, description: e.target.value })}
className='w-full p-2 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-900 dark:text-white' />
</div>
<Divider />
<p className='pr-2 text-sm font-semibold'>Cover Image <span className='italic font-light opacity-80'>(optional)</span></p>
<p className='pb-3 text-sm'>Upload an image to use as the cover image for your theme. <span className='italic font-light'>Recommended resolution: 640x128</span></p>
<div className="relative flex justify-center w-full gap-1 overflow-hidden transition rounded-lg aspect-theme group place-items-center bg-zinc-100 dark:bg-zinc-900">
<PlusIcon className={`transition pointer-events-none z-30 ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`} height={18} />
<span className={`dark:text-white pointer-events-none z-30 transition ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>{theme.coverImage ? 'Change' : 'Add'} cover image</span>
<input type="file" accept='image/*' onChange={(e) => {
const file = e.target.files?.[0];
e.target.value = '';
if (file) {
const reader = new FileReader();
reader.onload = async () => {
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
setTheme({ ...theme, coverImage: imageBlob });
};
reader.readAsDataURL(file);
}
}} className="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer" />
{
!theme.hideThemeName && theme.coverImage ?
<div className="absolute z-30 transition-opacity opacity-100 pointer-events-none group-hover:opacity-0">{theme.name}</div> : <></>
}
{
theme.coverImage &&
<>
<div className="absolute z-20 w-full h-full transition-opacity opacity-0 pointer-events-none group-hover:opacity-100 bg-black/20"></div>
<img src={URL.createObjectURL(theme.coverImage as Blob)} alt='Cover Image' className="absolute z-0 object-cover w-full h-full rounded" />
</>
}
</div>
<div className='flex items-center justify-between pt-4'>
<div>
<div className='pr-2 text-sm font-semibold'>Hide Name</div>
<div className='pr-2 text-[11px]'>Useful when your cover image contains text</div>
</div>
<Switch state={theme.hideThemeName} onChange={value => setTheme({ ...theme, hideThemeName: value })} />
</div>
<Divider />
{/* <div className='flex items-center justify-between'>
<div>
<div className='pr-2 text-sm font-semibold'>Custom Theme Colour</div>
<div className='pr-2 text-[11px]'>Allow users to change the theme colour</div>
</div>
<Switch state={theme.CanChangeColour} onChange={value => setTheme({ ...theme, CanChangeColour: value })} />
</div>
<div className='flex items-center justify-between pt-4'>
<div>
<div className='pr-2 text-sm font-semibold'>Custom Backgrounds</div>
<div className='pr-2 text-[11px]'>Allow users to set image and video backgrounds</div>
</div>
<Switch state={theme.allowBackgrounds} onChange={value => setTheme({ ...theme, allowBackgrounds: value })} />
</div>
<Divider /> */}
<div className='flex items-center justify-between'>
<div>
<div className='pr-2 text-sm font-semibold'>Force Theme</div>
<div className='pr-2 text-[11px]'>Force users to use either dark or light mode</div>
</div>
<Switch state={theme.forceDark == undefined ? false : true} onChange={value => setTheme({ ...theme, forceDark: value ? false : undefined })} />
</div>
{ theme.forceDark != undefined &&
<div className='flex items-center justify-between pt-4'>
<div>
<div className='pr-2 text-sm font-semibold'>Force {theme.forceDark ? 'Dark' : 'Light'} Mode</div>
<div className='pr-2 text-[11px]'>Force users to use {theme.forceDark ? 'dark' : 'light'} mode</div>
</div>
<button className='flex items-center justify-center p-2 transition rounded-lg bg-zinc-100 dark:bg-zinc-700' onClick={() => setTheme({ ...theme, forceDark: !theme.forceDark })}>
{theme.forceDark ? <MoonIcon className='w-6 h-6' /> : <SunIcon className='w-6 h-6' />}
</button>
</div>
}
<Divider />
<Accordion defaultOpened title='Default Theme Colour'>
<div className='p-2 mt-2 bg-white rounded-lg w-fit dark:bg-zinc-900'>
<ColorPicker
width={278}
disableDarkMode={true}
hideInputs={true}
value={theme.defaultColour}
onChange={(color: string) => setTheme({ ...theme, defaultColour: color })} />
</div>
</Accordion>
<Divider />
<h2 className='pb-1 text-lg font-semibold'>Custom Images</h2>
<p className='pb-3 text-sm'>Upload images to include them in your theme via CSS variables (gifs supported).</p>
{theme.CustomImages.map((image) => (
<div key={image.id} className="flex items-center h-16 py-2 mb-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900">
<div className="flex-1 h-full ">
<img src={URL.createObjectURL(image.blob)} alt={image.variableName} className="object-contain h-full rounded" />
</div>
<input
type="text"
value={image.variableName}
onChange={(e) => handleImageVariableChange(image.id, e.target.value)}
placeholder="CSS Variable Name"
className="flex-grow flex-[3] w-full p-2 transition-all duration-300 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-800/50 dark:text-white"
/>
<button onClick={() => handleRemoveImage(image.id)} className="p-2 ml-1 transition dark:text-white">
<XMarkIcon height={20} />
</button>
</div>
))}
<div className="relative flex justify-center w-full h-8 gap-1 overflow-hidden transition rounded-lg place-items-center bg-zinc-100 dark:bg-zinc-900">
<PlusIcon height={18} />
<span className='dark:text-white'>Add image</span>
<input type="file" accept='image/*' onChange={handleImageUpload} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
</div>
<Divider />
<Accordion defaultOpened title='Custom CSS'>
<CodeEditor
className='mt-2'
height='800px'
value={theme.CustomCSS}
setValue={CodeUpdate} />
</Accordion>
<Divider />
<button disabled={ theme.name === '' } onClick={saveTheme} className='w-full px-4 py-2 text-white transition bg-blue-500 rounded-lg dark:disabled:bg-zinc-700 disabled:bg-zinc-100 disabled:cursor-not-allowed dark:text-white'>
Save theme
</button>
</div>
</div>
);
}
function Divider() {
return <div className='w-full h-0.5 my-4 bg-zinc-200 dark:bg-zinc-700'></div>;
}
export default ThemeCreator;
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import TabbedContainer from '../components/TabbedContainer.svelte';
import Settings from './settings/general.svelte';
import Shortcuts from './settings/shortcuts.svelte';
import Theme from './settings/theme.svelte';
import browser from 'webextension-polyfill';
import { standalone as StandaloneStore } from '../utils/standalone.svelte';
import { onMount } from 'svelte'
import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState'
import { closeExtensionPopup, OpenAboutPage, OpenWhatsNewPopup } from "@/SEQTA"
import ColourPicker from '../components/ColourPicker.svelte'
import { settingsPopup } from '../hooks/SettingsPopup'
let devModeSequence = '';
const handleDevModeToggle = () => {
const handleKeyDown = (event: KeyboardEvent) => {
devModeSequence += event.key.toLowerCase();
if (devModeSequence.includes('dev')) {
document.removeEventListener('keydown', handleKeyDown);
settingsState.devMode = true;
alert('Dev mode is now enabled');
}
};
document.addEventListener('keydown', handleKeyDown);
setTimeout(() => {
document.removeEventListener('keydown', handleKeyDown);
devModeSequence = '';
}, 10000);
};
const openColourPicker = () => {
showColourPicker = true;
}
const openChangelog = () => {
OpenWhatsNewPopup();
closeExtensionPopup();
};
const openAbout = () => {
OpenAboutPage();
closeExtensionPopup();
};
let { standalone } = $props<{ standalone?: boolean }>();
let showColourPicker = $state<boolean>(false);
onMount(() => {
settingsPopup.addListener(() => {
showColourPicker = false;
});
if (!standalone) return;
initializeSettingsState();
StandaloneStore.setStandalone(true);
});
</script>
<div class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode ? 'dark' : ''} { standalone ? 'h-[600px]' : 'h-full rounded-xl' } overflow-clip">
<div class="relative flex flex-col h-full gap-2 bg-white overflow-clip dark:bg-zinc-800 dark:text-white">
<div class="grid border-b border-b-zinc-200/40 place-items-center">
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" onclick={handleDevModeToggle} />
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" onclick={handleDevModeToggle} />
{#if !standalone}
<button onclick={openChangelog} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-1 bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
<button onclick={openAbout} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</button>
{/if}
</div>
<TabbedContainer tabs={[
{ title: 'Settings', Content: Settings, props: { showColourPicker: openColourPicker } },
{ title: 'Shortcuts', Content: Shortcuts },
{ title: 'Themes', Content: Theme },
]} />
</div>
{#if showColourPicker}
<ColourPicker hidePicker={() => { showColourPicker = false }} />
{/if}
</div>
+176
View File
@@ -0,0 +1,176 @@
<script lang="ts">
import Switch from "../../components/Switch.svelte"
import Button from "../../components/Button.svelte"
import Slider from "../../components/Slider.svelte"
import Select from "@/interface/components/Select.svelte"
import browser from "webextension-polyfill"
import type { SettingsList } from "@/interface/types/SettingsProps"
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
</script>
{#snippet Setting({ title, description, Component, props }: SettingsList) }
<div class="flex items-center justify-between px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">{title}</h2>
<p class="text-xs">{description}</p>
</div>
<div>
<Component {...props} />
</div>
</div>
{/snippet}
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
{#each [
{
title: "Transparency Effects",
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
id: 1,
Component: Switch,
props: {
state: $settingsState.transparencyEffects,
onChange: (isOn: boolean) => settingsState.transparencyEffects = isOn
}
},
{
title: "Animated Background",
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
id: 2,
Component: Switch,
props: {
state: $settingsState.animatedbk,
onChange: (isOn: boolean) => settingsState.animatedbk = isOn
}
},
{
title: "Animated Background Speed",
description: "Controls the speed of the animated background.",
id: 3,
Component: Slider,
props: {
state: $settingsState.bksliderinput,
onChange: (value: number) => settingsState.bksliderinput = `${value}`
}
},
{
title: "Custom Theme Colour",
description: "Customise the overall theme colour of SEQTA Learn.",
id: 4,
Component: PickerSwatch,
props: {
onClick: showColourPicker
}
},
{
title: "Edit Sidebar Layout",
description: "Customise the sidebar layout.",
id: 5,
Component: Button,
props: {
onClick: () => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' }),
text: "Edit"
}
},
{
title: "Animations",
description: "Enables animations on certain pages.",
id: 6,
Component: Switch,
props: {
state: $settingsState.animations,
onChange: (isOn: boolean) => settingsState.animations = isOn
}
},
{
title: "Notification Collector",
description: "Uncaps the 9+ limit for notifications, showing the real number.",
id: 7,
Component: Switch,
props: {
state: $settingsState.notificationcollector,
onChange: (isOn: boolean) => settingsState.notificationcollector = isOn
}
},
{
title: "Lesson Alerts",
description: "Sends a native browser notification ~5 minutes prior to lessons.",
id: 8,
Component: Switch,
props: {
state: $settingsState.lessonalert,
onChange: (isOn: boolean) => settingsState.lessonalert = isOn
}
},
{
title: "12 Hour Time",
description: "Prefer 12 hour time format for SEQTA",
id: 9,
Component: Switch,
props: {
state: $settingsState.timeFormat === "12",
onChange: (isOn: boolean) => settingsState.timeFormat = isOn ? "12" : "24"
}
},
{
title: "Default Page",
description: "The page to load when SEQTA Learn is opened.",
id: 10,
Component: Select,
props: {
state: $settingsState.defaultPage,
onChange: (value: string) => settingsState.defaultPage = value,
options: [
{ value: 'home', label: 'Home' },
{ value: 'dashboard', label: 'Dashboard' },
{ value: 'timetable', label: 'Timetable' },
{ value: 'welcome', label: 'Welcome' },
{ value: 'messages', label: 'Messages' },
{ value: 'documents', label: 'Documents' },
{ value: 'reports', label: 'Reports' },
]
}
},
{
title: "BetterSEQTA+",
description: "Enables BetterSEQTA+ features",
id: 11,
Component: Switch,
props: {
state: $settingsState.onoff,
onChange: (isOn: boolean) => settingsState.onoff = isOn
}
}
] as option}
{@render Setting(option)}
{/each}
{#if $settingsState.devMode}
<div class="flex items-center justify-between px-4 py-3 mt-4 pt-[1.75rem]">
<div class="pr-4">
<h2 class="text-sm font-bold">Developer Mode</h2>
<p class="text-xs">Enables developer mode, allowing you to test new features and changes.</p>
</div>
<div>
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
</div>
</div>
<div class="flex items-center justify-between px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">Sensitive Hider</h2>
<p class="text-xs">Replace sensitive content with mock data</p>
</div>
<div>
<Button
onClick={() => hideSensitiveContent()}
text="Hide"
/>
</div>
</div>
{/if}
</div>
@@ -0,0 +1,159 @@
<script lang="ts">
import MotionDiv from '@/interface/components/MotionDiv.svelte';
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
import Switch from "@/interface/components/Switch.svelte"
import { onMount } from 'svelte';
let isLoaded = $state(false);
onMount(async () => {
// Wait for settingsState to be initialized
await new Promise<void>((resolve) => {
const checkState = () => {
if ($settingsState?.shortcuts) {
isLoaded = true;
resolve();
} else {
setTimeout(checkState, 100);
}
};
checkState();
});
});
const switchChange = (index: number) => {
const updatedShortcuts = [...settingsState.shortcuts];
updatedShortcuts[index].enabled = !updatedShortcuts[index].enabled;
settingsState.shortcuts = updatedShortcuts;
}
let isFormVisible = $state(false);
let newTitle = $state("");
let newURL = $state("");
const toggleForm = () => {
isFormVisible = !isFormVisible;
};
const formatUrl = (inputUrl: string) => {
const protocolRegex = /^(http:\/\/|https:\/\/|ftp:\/\/)/;
return protocolRegex.test(inputUrl) ? inputUrl : `https://${inputUrl}`;
};
const isValidTitle = (title: string) => title.trim() !== "";
const isValidURL = (url: string) => {
const pattern = new RegExp("^(https?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\-]+)*(?::\\d+)?(/[\\w\\-./]*)*$", "i");
return pattern.test(url);
};
const addNewCustomShortcut = () => {
if (isValidTitle(newTitle) && isValidURL(newURL)) {
const newShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon: newTitle[0] };
settingsState.customshortcuts = [...settingsState.customshortcuts, newShortcut];
newTitle = "";
newURL = "";
isFormVisible = false;
} else {
alert("Please enter a valid title and URL.");
}
};
const deleteCustomShortcut = (index: number) => {
settingsState.customshortcuts = settingsState.customshortcuts.filter((_, i) => i !== index);
};
</script>
{#snippet Shortcuts([index, Shortcut]: [string, { name: string, enabled: boolean }]) }
<div class="flex items-center justify-between px-4 py-3">
<div class="pr-4">
<h2 class="text-sm">{Shortcut.name}</h2>
</div>
<Switch state={Shortcut.enabled} onChange={() => switchChange(parseInt(index))} />
</div>
{/snippet}
<div class="flex flex-col pt-4 divide-y divide-zinc-100 dark:divide-zinc-700">
{#if isLoaded}
<div>
<MotionDiv
initial={{ opacity: 0, height: 0 }}
animate={isFormVisible ? { opacity: 1, height: 'auto' } : { opacity: 0, height: 0 }}
exit={{ opacity: 0, height: 0 }}
transition={{
type: 'spring',
config: { stiffness: 400, damping: 25 }
}}
>
{#if isFormVisible}
<div class="flex flex-col items-center">
<MotionDiv
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0, duration: 0.2 }}
class="w-full"
>
<input
class="w-full p-2 transition border-0 rounded-lg placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
type="text"
placeholder="Shortcut Name"
bind:value={newTitle}
/>
</MotionDiv>
<MotionDiv
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05, duration: 0.2 }}
class="w-full"
>
<input
class="w-full p-2 my-2 transition border-0 rounded-lg placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
type="text"
placeholder="URL eg. https://google.com"
bind:value={newURL}
/>
</MotionDiv>
</div>
{/if}
</MotionDiv>
<MotionDiv
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<button
class="w-full px-4 py-2 mb-4 text-[13px] dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50"
onclick={isFormVisible ? addNewCustomShortcut : toggleForm}
>
{#if isFormVisible}
Add
{:else}
Add Custom Shortcut
{/if}
</button>
</MotionDiv>
</div>
{#each Object.entries($settingsState.shortcuts) as shortcut}
{@render Shortcuts(shortcut)}
{/each}
<!-- Custom Shortcuts Section -->
{#each $settingsState.customshortcuts as shortcut, index}
<div class="flex items-center justify-between px-4 py-3">
{shortcut.name}
<button onclick={() => deleteCustomShortcut(index)}>
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/each}
{:else}
<div class="p-4 text-center">
Loading shortcuts...
</div>
{/if}
</div>
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts">
import BackgroundSelector from "@/interface/components/themes/BackgroundSelector.svelte"
import ThemeSelector from "@/interface/components/themes/ThemeSelector.svelte"
import { standalone } from "@/interface/utils/standalone.svelte"
// backgrounds
let selectedBackground = $state<string | null>(null);
let selectNoBackground = $state<() => void>(() => { });
let clearTheme = $derived(selectedBackground !== null);
let editMode = $state<boolean>(false);
</script>
<div class="py-4">
{#if !standalone.standalone}
<button
onclick={() => selectNoBackground()}
class="w-full px-4 py-2 mb-4 text-[13px] dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50">
{ clearTheme ? 'Clear Theme' : 'Select a Theme' }
</button>
<div class="relative w-full">
<button
onclick={() => editMode = !editMode}
class="absolute top-0 right-0 z-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{editMode ? '\ue9e4' : '\uec38'}</button>
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
<ThemeSelector isEditMode={editMode} />
</div>
{:else}
<div class="flex items-center justify-center w-full h-full">
<div class="text-lg">
Open SEQTA and use the embedded settings to access theme settings. 🫠
</div>
</div>
{/if}
</div>
+148
View File
@@ -0,0 +1,148 @@
<script lang="ts">
import { onMount } from 'svelte';
// Import existing components
import CoverSwiper from '../components/store/CoverSwiper.svelte';
import ThemeGrid from '../components/store/ThemeGrid.svelte';
import SkeletonLoader from '../components/SkeletonLoader.svelte';
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import type { Theme } from '../types/Theme'
import browser from 'webextension-polyfill'
import ThemeModal from '../components/store/ThemeModal.svelte'
import { StoreDownloadTheme } from '@/seqta/ui/themes/downloadTheme'
import { setTheme } from '@/seqta/ui/themes/setTheme'
import Header from '../components/store/Header.svelte'
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'
import { themeUpdates } from '../hooks/ThemeUpdates'
import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
import Backgrounds from '../components/store/Backgrounds.svelte'
// State variables
let searchTerm = $state('');
let themes = $state<Theme[]>([]);
let coverThemes = $state<Theme[]>([]);
let loading = $state(true);
let darkMode = $state(false);
let displayTheme = $state<Theme | null>(null);
let currentThemes = $state<string[]>([]);
let activeTab = $state('themes');
let error = $state<string | null>(null);
let selectedBackground = $state<string | null>(null);
const fetchCurrentThemes = async () => {
const themes = await getAvailableThemes();
currentThemes = themes.themes.filter(theme => theme !== null).map(theme => theme.id);
};
const setDisplayTheme = (theme: Theme | null) => {
displayTheme = theme;
};
const setSearchTerm = (term: string) => {
searchTerm = term;
};
const setActiveTab = (tab: string) => {
activeTab = tab;
};
// Fetch themes and initialize app
const fetchThemes = async () => {
try {
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${(new Date()).getTime()}`, { cache: 'no-store' });
const data = await response.json();
themes = data.themes;
// Shuffle for cover themes
const shuffled = [...themes].sort(() => 0.5 - Math.random());
coverThemes = shuffled.slice(0, 3);
loading = false;
} catch (error) {
console.error('Failed to fetch themes', error);
setTimeout(fetchThemes, 5000); // Retry after 5 seconds if failure occurs
}
};
// On mount
onMount(async () => {
await fetchThemes();
await fetchCurrentThemes();
darkMode = (await browser.storage.local.get('DarkMode')).DarkMode === 'true';
darkMode = $settingsState.DarkMode;
});
// Filter themes based on search term
let filteredThemes = $derived(themes.filter(theme =>
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
theme.description.toLowerCase().includes(searchTerm.toLowerCase())
));
$effect(() => {
loadBackground();
selectedBackground
});
$effect(() => {
if (error) {
console.error(error);
}
});
</script>
<div class="w-screen h-screen bg-white {darkMode ? 'dark' : ''}">
<div class="h-full overflow-y-scroll bg-zinc-200/50 dark:bg-zinc-900 dark:text-white pt-[4.25rem]">
<Header {searchTerm} {setSearchTerm} {darkMode} {activeTab} {setActiveTab} />
<div class={`px-12 h-full ${activeTab === 'backgrounds' ? 'pt-0' : 'pt-6 md:px-24 lg:px-48'}`}>
<!-- Loading State -->
{#if loading}
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
<SkeletonLoader width="100%" height="200px" />
</div>
{:else}
<!-- Themes Tab Content -->
{#if activeTab === 'themes'}
{#if searchTerm === ''}
<CoverSwiper {coverThemes} {setDisplayTheme} />
{/if}
<!-- ThemeGrid to display filtered themes -->
<ThemeGrid themes={filteredThemes} {searchTerm} {setDisplayTheme} />
{#if displayTheme}
<ThemeModal
currentThemes={currentThemes}
allThemes={themes}
theme={displayTheme}
{displayTheme}
{setDisplayTheme}
onInstall={async () => {
if (displayTheme) {
await StoreDownloadTheme({themeContent: displayTheme})
setTheme(displayTheme.id);
themeUpdates.triggerUpdate();
await fetchCurrentThemes();
}
}}
onRemove={async () => {
if (displayTheme?.id) {
console.debug('deleting theme', displayTheme.id);
deleteTheme(displayTheme.id)
themeUpdates.triggerUpdate();
await fetchCurrentThemes();
}
}}
/>
{/if}
{:else if activeTab === 'backgrounds'}
<Backgrounds {searchTerm} />
{/if}
{/if}
</div>
</div>
</div>
+358
View File
@@ -0,0 +1,358 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
import { fade } from 'svelte/transition';
import { type LoadedCustomTheme } from '@/types/CustomThemes'
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import { getTheme } from '@/seqta/ui/themes/getTheme'
import Divider from '@/interface/components/themeCreator/divider.svelte'
import Switch from '@/interface/components/Switch.svelte'
import Button from '@/interface/components/Button.svelte'
import Slider from '@/interface/components/Slider.svelte'
import ColourPicker from '../components/ColourPicker.svelte'
import CodeEditor from '../components/CodeEditor.svelte'
import {
handleImageUpload,
handleRemoveImage,
handleImageVariableChange,
handleCoverImageUpload
} from '../utils/themeImageHandlers';
import { ClearThemePreview, UpdateThemePreview } from '@/seqta/ui/themes/UpdateThemePreview'
import { saveTheme } from '@/seqta/ui/themes/saveTheme'
import { CloseThemeCreator } from '@/seqta/ui/ThemeCreator'
import { themeUpdates } from '../hooks/ThemeUpdates'
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
const { themeID } = $props<{ themeID: string }>()
let theme = $state<LoadedCustomTheme>({
id: uuidv4(),
name: '',
description: '',
defaultColour: 'blue',
CanChangeColour: true,
allowBackgrounds: true,
CustomCSS: '',
CustomImages: [],
coverImage: null,
isEditable: true,
hideThemeName: false,
forceDark: undefined
})
let closedAccordions = $state<string[]>([])
let themeLoaded = $state(false);
function toggleAccordion(title: string) {
if (closedAccordions.includes(title)) {
closedAccordions = closedAccordions.filter(t => t !== title);
} else {
closedAccordions = [...closedAccordions, title];
}
}
onMount(async () => {
disableTheme();
if (themeID) {
const tempTheme = await getTheme(themeID)
if (!tempTheme) return
// convert temptheme to LoadedCustomTheme
const loadedTheme = {
...tempTheme,
CustomImages: tempTheme.CustomImages.map(image => ({
...image,
url: image.blob ? URL.createObjectURL(image.blob) : null
})),
coverImageUrl: tempTheme.coverImage ? URL.createObjectURL(tempTheme.coverImage) : undefined
}
if (tempTheme) {
theme = loadedTheme
themeLoaded = true
}
} else {
themeLoaded = true
}
themeUpdates.triggerUpdate();
});
async function onImageUpload(event: Event) {
theme = await handleImageUpload(event, theme);
}
function onRemoveImage(imageId: string) {
theme = handleRemoveImage(imageId, theme);
}
function onImageVariableChange(imageId: string, variableName: string) {
theme = handleImageVariableChange(imageId, variableName, theme);
}
async function onCoverImageUpload(event: Event) {
theme = await handleCoverImageUpload(event, theme);
}
function submitTheme() {
const themeClone = JSON.parse(JSON.stringify(theme));
// re-insert blobs into themeClone
themeClone.CustomImages = theme.CustomImages.map((image) => ({
...image,
blob: image.blob
}))
themeClone.coverImage = theme.coverImage
ClearThemePreview();
saveTheme(themeClone);
themeUpdates.triggerUpdate();
CloseThemeCreator();
}
$effect(() => {
UpdateThemePreview(theme);
});
type SettingType = 'switch' | 'button' | 'slider' | 'colourPicker' | 'select' | 'codeEditor' | 'imageUpload' | 'conditional' | 'lightDarkToggle';
type SwitchProps = { state: boolean; onChange: (value: boolean) => void };
type ButtonProps = { onClick: () => void; text: string };
type SliderProps = { state: number; onChange: (value: number) => void; min?: number; max?: number };
type ColourPickerProps = { color: string; onChange: (color: string) => void };
type SelectProps = { options: Array<{ value: string; label: string }>; value: string; onChange: (value: string) => void };
type CodeEditorProps = { value: string; onChange: (value: string) => void };
type LightDarkToggleProps = { state: boolean; onChange: (value: boolean) => void };
type ConditionalProps = {
condition: boolean;
children: SettingItem;
};
type ComponentProps = SwitchProps | ButtonProps | SliderProps | ColourPickerProps | SelectProps | CodeEditorProps | LightDarkToggleProps | ConditionalProps;
type SettingItem = {
type: SettingType;
title: string;
description: string;
direction?: 'horizontal' | 'vertical';
props: ComponentProps;
};
</script>
{#snippet settingItem(item: SettingItem)}
{#if item.type === 'conditional'}
{#if (item.props as ConditionalProps).condition }
<div transition:slide={{ duration: 300 }}>
{@render settingItem((item.props as ConditionalProps).children)}
</div>
{/if}
{:else}
<div class="flex justify-between {item.direction === 'vertical' ? 'flex-col items-start' : 'items-center'} py-3">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onclick={() => { item.direction === 'vertical' && toggleAccordion(item.title) }}
onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title) }}
class="flex justify-between pr-4 {item.direction === 'vertical' ? 'cursor-pointer w-full select-none' : ''}">
<div>
<h2 class="text-sm font-bold">{item.title}</h2>
<p class="text-xs">{item.description}</p>
</div>
{#if item.direction === 'vertical'}
<div class="flex items-center justify-center h-full text-xl font-light text-zinc-500 dark:text-zinc-300">
<span class='font-IconFamily transition-transform duration-300 {closedAccordions.includes(item.title) ? 'rotate-180' : ''}'>{'\ue9e6'}</span>
</div>
{/if}
</div>
{#if !closedAccordions.includes(item.title)}
<div class="{item.direction === 'vertical' ? 'w-full mt-2' : ''}" transition:slide={{ duration: 300 }}>
{#if item.type === 'switch'}
<Switch {...(item.props as SwitchProps)} />
{:else if item.type === 'button'}
<Button {...(item.props as ButtonProps)} />
{:else if item.type === 'slider'}
<Slider {...(item.props as SliderProps)} />
{:else if item.type === 'colourPicker'}
{#key themeLoaded}
<ColourPicker savePresets={false} standalone={true} {...(item.props)} />
{/key}
{:else if item.type === 'codeEditor'}
{#key themeLoaded}
<CodeEditor {...(item.props as CodeEditorProps)} />
{/key}
{:else if item.type === 'imageUpload'}
{#each theme.CustomImages as image (image.id)}
<div class="flex items-center h-16 gap-2 px-2 py-2 mb-4 bg-white rounded-lg shadow-lg dark:bg-zinc-700">
<div class="h-full ">
<img src={image.url} alt={image.variableName} class="object-contain h-full rounded" />
</div>
<input
type="text"
bind:value={image.variableName}
oninput={(e) => onImageVariableChange(image.id, e.currentTarget.value)}
placeholder="CSS Variable Name"
class="flex-grow flex-[3] w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-600/50 focus:bg-zinc-300/50 dark:focus:bg-zinc-600"
/>
<button onclick={() => onRemoveImage(image.id)} class="p-2 transition dark:text-white">
<span class='text-xl font-IconFamily'>{'\ued8c'}</span>
</button>
</div>
{/each}
<div class="relative flex justify-center w-full h-8 gap-1 overflow-hidden transition rounded-lg place-items-center bg-zinc-200 dark:bg-zinc-700">
<span class='font-IconFamily'>{'\uec60'}</span>
<span class='dark:text-white'>Add image</span>
<input type="file" accept='image/*' onchange={onImageUpload} class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
</div>
{:else if item.type === 'lightDarkToggle'}
<button
class="relative px-4 py-1 overflow-hidden text-xl font-medium transition rounded-lg bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 font-IconFamily"
onclick={() => (item.props as LightDarkToggleProps).onChange(!(item.props as LightDarkToggleProps).state)}
>
{#key (item.props as LightDarkToggleProps).state}
<span
class="absolute"
in:fade={{ duration: 150 }}
out:fade={{ duration: 150 }}
>
{(item.props as LightDarkToggleProps).state ? '\uec12' : '\uecfe'}
</span>
{/key}
<span class='opacity-0'>{'\uec12'}</span>
</button>
{/if}
</div>
{/if}
</div>
{/if}
{/snippet}
<div class='h-screen overflow-y-scroll {$settingsState.DarkMode && "dark"} no-scrollbar'>
<div class='flex flex-col w-full min-h-screen p-2 bg-zinc-100 dark:bg-zinc-800 dark:text-white'>
<h1 class='text-xl font-semibold'>Theme Creator</h1>
<a href='https://betterseqta.gitbook.io/betterseqta-docs' target='_blank' class='text-sm font-light text-zinc-500 dark:text-zinc-400'>
<span class='no-underline font-IconFamily pr-0.5'>{'\ueb44'}</span>
<span class='underline'>
Need help? Check out the docs!
</span>
</a>
<Divider />
<div>
<div class='pb-2 text-sm'>Theme Name</div>
<input
id='themeName'
type='text'
placeholder='What is your theme called?'
bind:value={theme.name}
class='w-full p-2 mb-4 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600' />
</div>
<div>
<div class='pb-2 text-sm'>Description <span class='italic font-light opacity-80'>(optional)</span></div>
<textarea
id='themeDescription'
placeholder="Don't worry, this one's optional!"
bind:value={theme.description}
class='w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:outline-none focus:ring-1 focus:ring-zinc-100 dark:focus:ring-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600'></textarea>
</div>
<Divider />
<div class="relative flex justify-center w-full gap-1 overflow-hidden transition rounded-lg aspect-theme group place-items-center bg-zinc-200 dark:bg-zinc-700">
<div class={`transition pointer-events-none z-30 font-IconFamily ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>
{'\uec60'}
</div>
<span class={`dark:text-white pointer-events-none z-30 transition ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>{theme.coverImage ? 'Change' : 'Add'} cover image</span>
<input type="file" accept='image/*' onchange={onCoverImageUpload} class="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer" />
{#if !theme.hideThemeName && theme.coverImage}
<div class="absolute z-30 transition-opacity opacity-100 pointer-events-none group-hover:opacity-0">{theme.name}</div>
{/if}
{#if theme.coverImage}
<div class="absolute z-20 w-full h-full transition-opacity opacity-0 pointer-events-none group-hover:opacity-100 bg-black/20"></div>
<img src={theme.coverImageUrl} alt='Cover' class="absolute z-0 object-cover w-full h-full rounded" />
{/if}
</div>
<Divider />
{#each [
{
type: 'switch',
title: 'Hide Theme Name',
description: 'Useful when your cover image contains text',
props: {
state: theme.hideThemeName,
onChange: (value: boolean) => theme = { ...theme, hideThemeName: value }
}
},
{
type: 'switch',
title: 'Force Theme',
description: 'Force users to use either dark or light mode',
props: {
state: theme.forceDark !== undefined,
onChange: (value: boolean) => theme = { ...theme, forceDark: value ? false : undefined }
}
},
{
type: 'conditional',
props: {
condition: theme.forceDark !== undefined,
children: {
type: 'lightDarkToggle',
title: 'Mode',
description: 'Choose whether to force light or dark mode',
props: {
state: theme.forceDark === true,
onChange: (value: boolean) => theme = { ...theme, forceDark: value }
}
}
}
},
{
type: 'colourPicker',
title: 'Default Theme Colour',
description: 'Set the default color for your theme',
direction: 'vertical',
props: {
customState: theme.defaultColour,
customOnChange: (color: string) => theme = { ...theme, defaultColour: color }
}
},
{
type: 'imageUpload',
title: 'Custom Images',
description: 'Add custom images to your theme',
direction: 'vertical',
},
{
type: 'codeEditor',
title: 'Custom CSS',
description: 'Add custom CSS to your theme',
direction: 'vertical',
props: {
value: theme.CustomCSS,
onChange: (value: string) => { theme = { ...theme, CustomCSS: value } }
}
}
] as SettingItem[] as setting}
{@render settingItem(setting)}
{/each}
<button
onclick={submitTheme}
class="w-full px-4 py-2 mt-3 text-[13px] dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50">
Save Theme
</button>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More