Compare commits

..

260 Commits

Author SHA1 Message Date
SethBurkart123 51c265400c feat(app): improved input handling + better UI 2024-12-11 11:44:56 +11:00
SethBurkart123 6209b65afe feat(app): add icons to electron 2024-12-05 18:11:13 +11:00
SethBurkart123 bb388ab000 feat: electron building 2024-12-05 18:09:06 +11:00
SethBurkart123 f1512ba6e1 chore: clean up code 2024-12-05 14:43:12 +11:00
SethBurkart123 13fc077686 bump(version): 3.4.2 + changelog 2024-12-05 14:38:02 +11:00
SethBurkart123 7cf765121c fix(style): z-index of panels increased 2024-12-05 14:36:04 +11:00
SethBurkart123 4e393f14bb fix: enable assessments average by default 2024-12-05 14:35:23 +11:00
Seth Burkart 98347e038d Merge pull request #192 from ar-cyber/patch-16
Add mention for grades calc in readme
2024-12-03 17:23:35 +11:00
Andrew R f2bdb22ea8 fix(doc): make the readme mention the grade calculator 2024-12-03 13:52:16 +10:30
SethBurkart123 4afab2c52a perf: refactor AddBetterSEQTAElements for improved performance 2024-12-03 07:15:50 +11:00
SethBurkart123 4c6b43d7c7 fix(initial): make assessments average enabled by default 2024-12-03 06:59:06 +11:00
SethBurkart123 9e26d2c192 fix(changelog): versions and changelog fixed 2024-12-02 17:34:39 +11:00
SethBurkart123 7445e8be78 feat(changelog): update changelog + bump version 2024-12-02 12:30:25 +11:00
SethBurkart123 d1a876ff22 fix(home): sidebar button sometimes not selecting 2024-12-02 12:25:14 +11:00
SethBurkart123 e2176ea2fa fix(assessments): placing subject average trice 2024-12-02 12:23:47 +11:00
SethBurkart123 a999e4384b feat(notices): add animations to notices 2024-12-02 12:17:39 +11:00
SethBurkart123 4bf5420140 fix(eventManager): not triggering for already created elements' 2024-12-02 12:17:28 +11:00
SethBurkart123 8fb29f7f21 feat(settings): add subject average setting 2024-12-02 12:01:03 +11:00
SethBurkart123 44e3ed34d0 feat(assessments): add subject averages 2024-12-02 11:58:42 +11:00
SethBurkart123 32228ee4db style(changelog): reduce video padding for better screen realestate 2024-12-02 11:11:49 +11:00
SethBurkart123 1692bd3e92 fix(home): notices date picker not working 2024-12-02 11:11:03 +11:00
SethBurkart123 71cf9dbca8 feat(messages): improvements to direct message animations 2024-12-02 11:01:54 +11:00
SethBurkart123 372b591b16 feat(home): async home page loading 2024-12-02 10:58:24 +11:00
SethBurkart123 7578ecee74 chore(logging): improve logging 2024-12-02 09:27:33 +11:00
SethBurkart123 a2b4f81b86 feat(home): add skeleton loaders to homepage 2024-12-02 09:26:49 +11:00
SethBurkart123 fcd95f6823 perf(homePage): add fragmentation and refactor code 2024-11-30 21:55:10 +11:00
SethBurkart123 f2ea7c8104 fix: builds failing 2024-11-29 17:53:47 +11:00
SethBurkart123 379a3ebda0 chore: code cleanup 2024-11-29 17:34:16 +11:00
SethBurkart123 d1850e8ddb chore: fix CI/CD pipeline 2024-11-29 17:24:02 +11:00
SethBurkart123 88a87692cd fix(icon): discord icon scaled incorrectly 2024-11-29 17:18:42 +11:00
Seth Burkart 4e6e4870b0 Merge pull request #189 from MEGA-Dawg68/main
Added Discord icon
2024-11-29 17:15:53 +11:00
SethBurkart123 18f215fa5f chore: code cleanup 2024-11-29 17:14:32 +11:00
SethBurkart123 2ea8ada439 fix(popups): backdrop filter failing to appear during animation 2024-11-29 17:13:59 +11:00
SethBurkart123 34b2501617 chore(changelog): update changelog 2024-11-29 14:57:41 +11:00
SethBurkart123 88d4d3aa11 feat(news): add animations to news page 2024-11-29 14:57:17 +11:00
SethBurkart123 5ed3a05f6a perf(news): improved news page performance 2024-11-29 14:52:26 +11:00
SethBurkart123 430f158957 style(news): improved news styles 2024-11-29 14:38:41 +11:00
SethBurkart123 547caabc45 bump(version): 3.4.1 + changelog 2024-11-29 14:25:58 +11:00
SethBurkart123 f6e549c5da feat(animations): refine home page animations 2024-11-29 11:40:37 +11:00
SethBurkart123 dc1ae9c0a1 feat(animations): migrate to motion-one@11 2024-11-29 11:32:43 +11:00
SethBurkart123 34306e77cf fix(studentInfo): some cases year may be undefined 2024-11-29 10:29:35 +11:00
SethBurkart123 00c9f03827 bump: version to 3.4.0.4 2024-11-27 09:23:40 +11:00
SethBurkart123 6c93477998 fix: background migration not working properly 2024-11-27 09:22:34 +11:00
MEGA_Dawg68 9784b6162f Added Discord icon 2024-11-25 19:57:45 +08:00
SethBurkart123 96cf8e3eac feat: clean up CI/CD piplelines 2024-11-15 17:24:26 +11:00
SethBurkart123 0e23ea0cc3 fix: build running twice in CI/CD 2024-11-15 17:23:34 +11:00
SethBurkart123 7dfe347562 chore: move install to build section 2024-11-15 17:14:15 +11:00
SethBurkart123 21a8472c94 fix: themes failing to install from coverImage decoding error 2024-11-15 16:35:28 +11:00
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
sethburkart123 f996e4bf19 bump: version to hotfix 2024-08-28 15:05:05 +10:00
sethburkart123 30c5a823d8 fix: assessments not loading after notices error #150 2024-08-28 15:02:09 +10:00
sethburkart123 10977247cc fix: manifest description too long 2024-08-27 17:39:26 +10:00
171 changed files with 10050 additions and 4201 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.
@@ -24,7 +24,7 @@ jobs:
- name: Build
run: |
npm install
npm install --legacy-peer-deps
npm run build
- name: Zip dist folder
+4 -11
View File
@@ -7,24 +7,17 @@ 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
# Electron
electron-dist/
View File
+9 -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>
@@ -40,6 +43,7 @@
- Easier Access Notices
- Assessments
- Options to remove certain items from the side menu
- Grades calculator
- Fully customisable themes and an offical theme store
- Notification for next lesson (sent 5 minutes before end of the lesson)
- Browser Support
@@ -48,18 +52,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 +110,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 +119,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 +129,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.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+162
View File
@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html>
<head>
<title>BetterSEQTA Settings</title>
<style>
:root {
--background-primary: #ffffff;
--background-secondary: #e5e7eb;
--text-primary: black;
--theme-primary: #4F46E5;
--theme-hover: #4338CA;
}
@media (prefers-color-scheme: dark) {
:root {
--background-primary: #232323;
--background-secondary: #1a1a1a;
--text-primary: white;
--theme-primary: #6366F1;
--theme-hover: #818CF8;
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
background: var(--background-primary);
color: var(--text-primary);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.container {
width: 100%;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
}
h1 {
font-size: 1.75rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
font-size: 1rem;
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: var(--text-primary);
}
input[type="url"] {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--background-secondary);
border-radius: 8px;
font-size: 1rem;
background: var(--background-secondary);
color: var(--text-primary);
transition: all 0.2s;
}
input[type="url"]:focus {
outline: none;
border-color: var(--theme-primary);
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
}
button {
margin-top: 1rem;
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 8px;
background: var(--theme-primary);
color: white;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: var(--theme-hover);
}
#error-message {
color: #EF4444;
font-size: 0.875rem;
margin-top: 0.5rem;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>BetterSEQTA Settings</h1>
<div class="subtitle">It's time to get started! To begin type in your school's SEQTA URL below.</div>
<label for="seqtaUrl">SEQTA Website URL</label>
<input type="url" id="seqtaUrl" placeholder="Enter your school's SEQTA URL (e.g https://seqta.school.edu.au)" required>
<div id="error-message"></div>
<button onclick="saveSettings()">Save Settings</button>
</div>
<script>
const electron = window.require('electron');
const { ipcRenderer } = electron;
const Store = window.require('electron-store');
const store = new Store();
// Load saved URL on page load
window.addEventListener('DOMContentLoaded', () => {
const savedUrl = store.get('seqtaUrl') || '';
document.getElementById('seqtaUrl').value = savedUrl;
});
// Handle error messages from main process
ipcRenderer.on('seqta-url-error', (event, message) => {
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
errorElement.style.display = 'block';
});
// Save settings
function saveSettings() {
const url = document.getElementById('seqtaUrl').value;
const errorElement = document.getElementById('error-message');
errorElement.style.display = 'none';
if (url) {
ipcRenderer.send('set-seqta-url', url);
} else {
errorElement.textContent = 'Please enter a URL';
errorElement.style.display = 'block';
}
}
// Handle enter key
document.getElementById('seqtaUrl').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
saveSettings();
}
});
</script>
</body>
</html>
+333
View File
@@ -0,0 +1,333 @@
import { app, BrowserWindow, ipcMain, session, Menu } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import Store from 'electron-store';
// Fix for __dirname in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const store = new Store();
let mainWindow = null;
let settingsWindow = null;
// CSS to inject
const customCSS = `
#alertBar {
display: none !important;
}
/* Match SEQTA's styling */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif !important;
}
`;
// Create the application menu
function createAppMenu() {
const isMac = process.platform === 'darwin';
const template = [
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
{
label: 'Settings',
submenu: [
{
label: 'Configure SEQTA URL',
accelerator: isMac ? 'Cmd+,' : 'Ctrl+,',
click: () => {
createSettingsWindow();
}
}
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
// Validate SEQTA URL
function isValidSeqtaUrl(url) {
try {
const urlObj = new URL(url);
// Only ensure it's a valid HTTPS URL
return urlObj.protocol === 'https:';
} catch {
return false;
}
}
// Get the correct path for the extension based on whether we're in development or production
function getExtensionPath() {
if (app.isPackaged) {
// In production, the extension is in the resources directory
return path.join(process.resourcesPath, 'chrome-extension');
} else {
// In development, the extension is in the dist directory
return path.join(__dirname, '..', 'dist', 'chrome');
}
}
// Load the Chrome extension
async function loadExtension() {
try {
const extensionPath = getExtensionPath();
console.log('Loading extension from:', extensionPath);
await session.defaultSession.loadExtension(extensionPath, {
allowFileAccess: true
});
console.log('Extension loaded successfully!');
} catch (err) {
console.error('Failed to load extension:', err);
}
}
function createMainWindow() {
console.log('🚀 Creating main window...');
if (mainWindow) {
if (!mainWindow.isDestroyed()) {
console.log('✨ Existing window found, focusing it');
mainWindow.focus();
return mainWindow;
}
console.log('🔄 Old window was destroyed, creating new one');
}
console.log('📦 Initializing new BrowserWindow');
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
backgroundThrottling: false,
enableWebSQL: false,
webgl: false,
offscreen: false
},
show: false,
backgroundColor: '#ffffff'
});
const seqtaUrl = store.get('seqtaUrl');
console.log('📍 Stored SEQTA URL:', seqtaUrl);
// Register keyboard shortcut for settings
mainWindow.webContents.on('before-input-event', (event, input) => {
if ((input.meta || input.control) && input.key === ',') {
createSettingsWindow();
}
});
// Inject CSS when the page loads
mainWindow.webContents.on('did-finish-load', () => {
console.log('🎨 Page loaded, injecting CSS');
mainWindow.webContents.insertCSS(customCSS).catch(err => {
console.error('Failed to inject CSS:', err);
});
});
// Only show window when it's ready
mainWindow.once('ready-to-show', () => {
console.log('🎉 Window ready to show!');
mainWindow.show();
mainWindow.focus();
});
if (seqtaUrl) {
if (!isValidSeqtaUrl(seqtaUrl)) {
console.error('❌ Invalid SEQTA URL stored:', seqtaUrl);
createSettingsWindow();
return;
}
console.log('🌐 Loading SEQTA URL:', seqtaUrl);
mainWindow.loadURL(seqtaUrl)
.then(() => {
console.log('✅ Successfully loaded SEQTA URL');
mainWindow.show();
mainWindow.focus();
})
.catch(err => {
console.error('❌ Failed to load SEQTA URL:', err);
createSettingsWindow();
});
} else {
console.log('⚙️ No SEQTA URL found, opening settings');
createSettingsWindow();
}
return mainWindow;
}
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({
width: 600,
height: 400,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
show: false,
backgroundColor: '#ffffff'
});
const settingsPath = path.join(__dirname, 'index.html');
settingsWindow.loadFile(settingsPath);
settingsWindow.once('ready-to-show', () => {
settingsWindow.show();
settingsWindow.focus();
});
// Only enable DevTools in development
if (process.env.NODE_ENV === 'development') {
settingsWindow.webContents.openDevTools();
}
settingsWindow.on('closed', () => {
settingsWindow = null;
});
}
// Performance optimization: Disable hardware acceleration if running on low-end device
if (process.platform !== 'darwin') { // Skip for macOS
app.disableHardwareAcceleration();
}
// Performance optimization: Disable smooth scrolling
app.commandLine.appendSwitch('disable-smooth-scrolling');
// Wait for app to be ready before creating windows
app.whenReady().then(async () => {
createAppMenu();
await loadExtension();
createMainWindow();
});
// Performance optimization: Quit immediately instead of gracefully
app.on('window-all-closed', () => {
app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
// Format and validate SEQTA URL
function formatAndValidateUrl(url) {
// Remove any whitespace
url = url.trim();
// If no protocol specified, add https://
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
// If it's http://, upgrade to https://
if (url.startsWith('http://')) {
url = 'https://' + url.slice(7);
}
try {
const urlObj = new URL(url);
// Ensure it's https
if (urlObj.protocol !== 'https:') {
throw new Error('URL must use HTTPS');
}
return { isValid: true, url: url };
} catch (error) {
return { isValid: false, url: url, error: error.message };
}
}
// Handle setting the SEQTA URL
ipcMain.on('set-seqta-url', (event, url) => {
console.log('🔧 Received new SEQTA URL:', url);
const { isValid, url: formattedUrl, error } = formatAndValidateUrl(url);
if (!isValid) {
console.error('❌ Invalid URL format:', error);
event.reply('seqta-url-error', 'Please enter a valid URL');
return;
}
console.log('💾 Saving URL to store:', formattedUrl);
store.set('seqtaUrl', formattedUrl);
// Create main window if it doesn't exist
if (!mainWindow || mainWindow.isDestroyed()) {
console.log('🆕 Creating new main window');
createMainWindow();
} else {
console.log('🔄 Loading new URL in existing window:', formattedUrl);
mainWindow.loadURL(formattedUrl).then(() => {
console.log('✅ URL loaded successfully');
console.log('🎨 Injecting CSS and settings button');
mainWindow.webContents.insertCSS(customCSS).catch(err => {
console.error('Failed to inject CSS:', err);
});
mainWindow.webContents.executeJavaScript(`
if (!document.getElementById('bsp-settings-button')) {
document.body.insertAdjacentHTML('beforeend', ${JSON.stringify(settingsButtonHTML)});
document.getElementById('bsp-settings-button').addEventListener('click', () => {
window.postMessage('open-settings', '*');
});
}
`).catch(err => {
console.error('Failed to inject settings button:', err);
});
console.log('👀 Showing and focusing window');
mainWindow.show();
mainWindow.focus();
}).catch(err => {
console.error('❌ Failed to load SEQTA URL:', err);
event.reply('seqta-url-error', 'Failed to load SEQTA. Please check your connection and URL.');
});
}
// Close settings window if it exists
if (settingsWindow && !settingsWindow.isDestroyed()) {
console.log('🚪 Closing settings window');
settingsWindow.close();
}
});
+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"
}
]
}
-46
View File
@@ -1,46 +0,0 @@
{
"manifest_version": 3,
"name": "BetterSEQTA+",
"version": "3.3.0",
"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"],
"host_permissions": ["https://newsapi.org/", "*://*/*"],
"background": {
"service_worker": "src/background.ts"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["src/SEQTA.ts"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": ["src/interface/index.html"],
"matches": ["*://*/*"]
},
{
"resources": ["src/seqta/ui/background/background.html"],
"matches": ["*://*/*"]
},
{
"resources": ["*://*/*"],
"matches": ["*://*/*"]
}
]
}
+115 -47
View File
@@ -1,15 +1,25 @@
{
"name": "betterseqtaplus",
"version": "3.3.0",
"version": "3.4.2",
"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!",
"main": "electron/main.js",
"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",
"electron-dev": "electron .",
"electron-build": "electron-builder",
"electron-pack": "npm run build:chrome && electron-builder --dir",
"electron-dist": "npm run build:chrome && electron-builder"
},
"targets": {
"prod": {
@@ -19,64 +29,122 @@
}
},
"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",
"@babel/plugin-transform-runtime": "^7.25.9",
"@babel/runtime": "^7.26.0",
"@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",
"electron": "^33.2.1",
"electron-builder": "^25.1.8",
"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",
"caniuse-lite": "^1.0.30001684",
"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",
"electron-store": "^10.0.0",
"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": "^11.12.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"
},
"build": {
"appId": "com.betterseqta.app",
"productName": "BetterSEQTA",
"directories": {
"output": "electron-dist",
"buildResources": "build"
},
"files": [
"dist/**/*",
"electron/**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
"!.editorconfig",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
"!**/{appveyor.yml,.travis.yml,circle.yml}",
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
],
"extraResources": [
{
"from": "dist/chrome",
"to": "chrome-extension",
"filter": ["**/*"]
}
],
"mac": {
"category": "public.app-category.education",
"target": ["dmg", "zip"],
"icon": "build/icon.icns"
},
"win": {
"target": "nsis",
"icon": "build/icon.ico"
},
"linux": {
"target": "AppImage",
"icon": "build/icon.png"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+719 -418
View File
File diff suppressed because it is too large Load Diff
+2 -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) => {
@@ -167,6 +167,7 @@ const DefaultValues: SettingsState = {
originalSelectedColor: '',
DarkMode: true,
animations: true,
assessmentsAverage: true,
defaultPage: 'home',
shortcuts: [
{
+1 -1
View File
@@ -15,7 +15,7 @@
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
*/
@import './injected/popup.scss';
@use 'injected/popup.scss';
html {
background: #161616 !important;
+206 -65
View File
@@ -1,9 +1,10 @@
@use "sass:meta";
@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';
@include meta.load-css("injected/sidebar-animation.scss");
@include meta.load-css("injected/theme.scss");
@include meta.load-css("injected/transparency.scss");
:root {
background: var(--better-main) !important;
@@ -16,7 +17,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 +32,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 +41,8 @@ html {
height: 100%;
visibility: visible !important;
}
#themeCreatorIframe {
#themeCreator {
position: fixed;
right: 0;
height: 100%;
@@ -62,6 +65,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 +119,10 @@ html {
z-index: 10000;
}
.modaliser-container {
backdrop-filter: none !important;
}
.connectedNotificationsWrapper > div > button > svg > g {
fill: var(--theme-primary) !important;
}
@@ -165,7 +206,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 +217,8 @@ html {
}
.dark .dashboard section {
input, select {
input,
select {
background: rgba(255, 255, 255, 0.1);
}
}
@@ -197,7 +240,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 +282,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 +300,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 +341,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 +386,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 +497,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 {
@@ -1057,9 +1111,9 @@ div > ol:has(.uiFileHandlerWrapper) {
display: flex;
flex-direction: column;
background: var(--background-primary);
transition: 200ms;
box-shadow: inset 0px 5px 20px 1px rgba(0, 0, 0, 0.3);
padding-bottom: 25px;
transition: none;
color: var(--text-primary);
}
.dummynotice {
@@ -1290,7 +1344,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 +1552,13 @@ iframe.userHTML {
.Collapsible__Collapsible___3O8P3 > .Collapsible__header___-Afvq {
background: none;
}
.AssessmentList__AssessmentList___1GdCl > .AssessmentList__searchFilter___3N70o + .AssessmentList__items___3LcmQ {
.Collapsible__Collapsible___3O8P3 > .Collapsible__content___2c6of.Collapsible__enterActive___3b2ow,
.Collapsible__Collapsible___3O8P3 > .Collapsible__content___2c6of.Collapsible__exitActive___3rFL1 {
animation-timing-function: ease-out !important;
}
.AssessmentList__AssessmentList___1GdCl
> .AssessmentList__searchFilter___3N70o
+ .AssessmentList__items___3LcmQ {
color: var(--text-primary);
}
.Thermoscore__Thermoscore___2tWMi {
@@ -1546,19 +1608,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 +1620,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 +1656,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 +1787,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;
}
@@ -1858,6 +1933,7 @@ div.bar.flat {
transition: background-color 0.5s ease-in-out;
background-color: rgba(0, 0, 0, 0);
transition-duration: 500ms !important;
z-index: 22 !important;
}
.uiSlidePane.shown > .pane {
transform: translatey(0%) !important;
@@ -1914,7 +1990,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 +2033,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 +2080,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 +2102,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 +2178,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);
}
@@ -2104,24 +2194,19 @@ li.MessageList__unread___3imtO {
overflow: hidden;
width: 100%;
padding: 0 !important;
margin: 0px auto 30px !important;
background: var(--background-primary) !important;
color: var(--text-primary);
display: flex;
flex-direction: row;
-webkit-box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.3);
box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.3);
&:hover {
.ArticleText a {
text-decoration: underline;
}
.articleimage {
width: 35%;
background-position: center;
background-size: cover;
min-height: 18em;
background-repeat: no-repeat;
}
.NewsArticle img {
width: 35%;
}
.ArticleText a {
padding: 10px 20px;
margin: 0;
@@ -2129,20 +2214,36 @@ li.MessageList__unread___3imtO {
font-size: 2em;
background: none;
}
.NewsArticle:hover > .ArticleText a {
text-decoration: underline;
}
.ArticleText p {
padding: 10px 20px;
margin: 0;
font-size: 1.5em;
}
.ArticleText {
display: flex;
flex-direction: column;
width: 65%;
height: 100%;
}
.articleimage {
width: 35%;
background-position: center;
background-size: cover;
min-height: 18em;
background-repeat: no-repeat;
}
}
#news-container {
gap: 16px;
> h1 {
margin-bottom: 12px;
}
}
.editmenu {
position: absolute;
right: 0;
@@ -2509,20 +2610,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 +2683,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 +2694,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;
@@ -2623,7 +2729,7 @@ li.MessageList__unread___3imtO {
display: flex;
flex-direction: column;
color: var(--text-primary);
transition: 200ms;
transition: 200ms, background-color 0s;
border-radius: 16px;
}
.dark .upcoming-items {
@@ -2900,14 +3006,12 @@ li.MessageList__unread___3imtO {
width: 96%;
display: flex;
margin: 0 auto;
padding-bottom: 16px;
}
.whatsnewImg {
margin: 8px auto;
width: 90%;
border-radius: 16px;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
}
.whatsnewTextContainer {
display: flex;
@@ -2977,9 +3081,46 @@ 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;
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.loading {
&.upcoming-items,
&.day-container {
background: linear-gradient(90deg,
var(--background-primary) 0%,
var(--background-secondary) 50%,
var(--background-primary) 100%
);
background-size: 1000px 100%;
animation: shimmer 2s infinite linear;
}
&.upcoming-items {
height: 35em;
}
}
+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,102 @@
<script lang="ts">
import { onMount } from 'svelte'
import ColourPicker from './ColourPicker.tsx';
import ReactAdapter from './utils/ReactAdapter.svelte';
import { animate } from 'motion';
import { delay } from '@/seqta/utils/delay.ts'
const { hidePicker, standalone = false, savePresets = true, customOnChange = null, customState = null } = $props<{
hidePicker?: () => void,
standalone?: boolean,
savePresets?: boolean,
customOnChange?: (color: string) => void,
customState?: string
}>();
let background = $state<HTMLDivElement | null>(null);
let content = $state<HTMLDivElement | null>(null);
const closePicker = async () => {
if (standalone) return;
if (!background || !content) return;
animate(
content,
{ scale: [1, 0.4], opacity: [1, 0] },
{
type: 'spring',
stiffness: 400,
damping: 30
}
);
animate(
background,
{ opacity: [1, 0] },
{ ease: [0.4, 0, 0.2, 1] }
);
await delay(400);
hidePicker();
}
onMount(() => {
if (standalone) return;
if (!background || !content) return;
animate(
background,
{ opacity: [0, 1] },
{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }
);
animate(
content,
{ scale: [0.4, 1], opacity: [0, 1] },
{
type: 'spring',
stiffness: 400,
damping: 30
}
);
const handleEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePicker();
}
};
document.addEventListener('keydown', handleEscapeKey);
return () => {
document.removeEventListener('keydown', handleEscapeKey);
};
});
function handleBackgroundClick(event: MouseEvent) {
if (event.target === background) {
closePicker();
}
}
</script>
{#if standalone}
<div class="h-auto rounded-xl overflow-clip">
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
</div>
{:else}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={background}
class="absolute top-0 left-0 z-50 flex items-center justify-center w-full h-full cursor-pointer bg-black/20"
onclick={handleBackgroundClick}
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
>
<div
bind:this={content}
class="h-auto p-4 bg-white border shadow-lg cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
>
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
</div>
</div>
{/if}
+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;
+79
View File
@@ -0,0 +1,79 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { animate as motionAnimate } from 'motion';
let { initial, animate, exit, transition, children, class: className } = $props<{
initial?: any,
animate?: any,
exit?: any,
transition?: any,
children?: any,
class?: string
}>();
let divElement: HTMLElement;
const playAnimation = (keyframe: any) => {
if (divElement && keyframe) {
let finalKeyframe = { ...keyframe };
if (finalKeyframe.height === 'auto') {
const prevHeight = divElement.style.height;
const prevVisibility = divElement.style.visibility;
divElement.style.height = 'auto';
divElement.style.visibility = 'hidden';
divElement.style.position = 'absolute';
const autoHeight = divElement.offsetHeight;
divElement.style.height = prevHeight;
divElement.style.visibility = prevVisibility;
divElement.style.position = '';
finalKeyframe.height = `${autoHeight}px`;
}
const defaultSpringConfig = { stiffness: 250, damping: 25 };
const animation = motionAnimate(
[divElement],
finalKeyframe,
{
type: 'spring',
stiffness: transition?.stiffness || defaultSpringConfig.stiffness,
damping: transition?.damping || defaultSpringConfig.damping
}
);
return animation;
}
return Promise.resolve();
};
onMount(async () => {
if (initial) {
Object.assign(divElement.style, initial);
await playAnimation(animate || {});
} else if (animate) {
await playAnimation(animate);
}
});
$effect(() => {
if (animate) {
playAnimation(animate);
}
});
onDestroy(async () => {
if (exit) {
await playAnimation(exit);
}
});
</script>
<div class={className} bind:this={divElement} style="will-change: transform, opacity;">
{#if children}
{@render children()}
{/if}
</div>
-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>
+51
View File
@@ -0,0 +1,51 @@
<script lang="ts">
import { animate } from 'motion';
import { standalone } from '../utils/standalone.svelte'
let { state, onChange } = $props<{ state: boolean, onChange: (newState: boolean) => void }>();
let handle: HTMLElement | null = null;
const springParams = {
stiffness: 600,
damping: 30,
};
const animateSwitch = (enabled: boolean) => {
if (!handle) return;
animate(
handle,
{
x: enabled ? (standalone.standalone ? 24 : 20) : 0,
},
{
type: 'spring',
stiffness: springParams.stiffness,
damping: springParams.damping,
}
);
};
// Trigger animation whenever state changes
$effect(() => animateSwitch(state));
</script>
<div
class="flex w-14 p-1 cursor-pointer transition-all duration-150 rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch select-none"
data-ison={state}
onclick={() => onChange(!state)}
onkeydown={(e) => e.key === "Enter" && onChange(!state)}
role="switch"
aria-checked={state}
tabindex="0"
>
<div
bind:this={handle}
class="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
></div>
</div>
<style>
.switch[data-ison="true"] {
background-color: #30D259;
}
</style>
-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,126 @@
<script lang="ts">
import type { Theme } from '@/interface/types/Theme'
import { fade } from 'svelte/transition';
import { animate } from 'motion';
let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme } = $props<{
theme: Theme | null;
currentThemes: string[];
setDisplayTheme: (theme: Theme | null) => void;
onInstall: (themeId: string) => void;
onRemove: (themeId: string) => void;
allThemes: Theme[];
displayTheme: Theme | null;
}>();
let installing = $state(false);
let modalElement: HTMLElement;
// Function to get related themes
function getRelatedThemes() {
return allThemes
.filter((t: Theme) => t.id !== theme.id)
.sort((a: Theme, b: Theme) => a.name.localeCompare(theme.name) - b.name.localeCompare(theme.name))
.slice(0, 4);
}
$effect(() => {
if (displayTheme) {
animate(
modalElement,
{ y: [500, 0], opacity: [0, 1] },
{
type: 'spring',
stiffness: 150,
damping: 20
}
);
}
});
const hideModal = (relatedTheme?: Theme | null) => {
animate(
modalElement,
{ y: [10, 500], opacity: [1, 0] },
{
type: 'spring',
stiffness: 150,
damping: 20
}
);
setTimeout(() => {
setDisplayTheme(relatedTheme ?? null);
}, 100);
}
</script>
<div
class="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;

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