mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f90ef2a8 | |||
| 9bcc94aa8a | |||
| ff2431f269 | |||
| b442194bc5 | |||
| b59c0eae25 | |||
| e895ce9f6b | |||
| 7192f41535 | |||
| f1b707ab25 | |||
| 7f47cb8183 | |||
| 7f5d138bc9 | |||
| cef0f29640 | |||
| 157343dda9 | |||
| 7705c0a3cd | |||
| 7def7b190c | |||
| c294fb7369 | |||
| 0dbbef0eb1 | |||
| c3c747d996 | |||
| cdc8062275 | |||
| 1857b5ff01 | |||
| 700e3ebb48 | |||
| 16b9610301 | |||
| 7d11e203a6 | |||
| 530f07e640 | |||
| 08586781ce | |||
| 3ca5a49769 | |||
| 886c79b3ee | |||
| 30aa39142d | |||
| 4188ef0d67 | |||
| ad9a013b00 | |||
| cd1f954cc7 | |||
| 6ef6c986dc | |||
| f2e28175a0 | |||
| 3ddcb204ef | |||
| 766f0e6d3f | |||
| f1fcba58ef | |||
| dba2d13bb3 | |||
| 30bf345b86 | |||
| 0e98f52058 | |||
| f89508deb2 | |||
| c7b69ad97b | |||
| 2ef8bb215a | |||
| 16273cf012 | |||
| 13d3ccd8e4 | |||
| 7ebc4db9db | |||
| ed9d662ba4 | |||
| 8647e0b272 | |||
| d93abec615 | |||
| 339b409937 | |||
| 860916a5b8 |
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a target="_blank" href="https://chrome.google.com/webstore/detail/betterseqta%20/afdgaoaclhkhemfkkkonemoapeinchel"><img src="https://user-images.githubusercontent.com/95666457/149519713-159d7ef7-2c21-4034-a616-f037ff46d9a4.png" alt="ChromeDownload" width="250"></a>
|
<a target="_blank" href="https://chrome.google.com/webstore/detail/betterseqta%20/afdgaoaclhkhemfkkkonemoapeinchel"><img src="https://user-images.githubusercontent.com/95666457/149519713-159d7ef7-2c21-4034-a616-f037ff46d9a4.png" alt="ChromeDownload" width="250"></a>
|
||||||
<a target="_blank" href="https://discord.gg/YzmbnCDkat"><img src="https://github.com/SethBurkart123/EvenBetterSEQTA/assets/108050083/23055730-b16e-44c0-9bef-221d8545af92" width="240" style="border-radius:10%;" /></a>
|
<a target="_blank" href="https://discord.gg/YzmbnCDkat"><img src="https://github.com/BetterSEQTA/BetterSEQTA-Plus/assets/108050083/23055730-b16e-44c0-9bef-221d8545af92" width="240" style="border-radius:10%;" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -64,7 +64,7 @@ Don't worry- if you get stuck feel free to ask around in the [discord](https://d
|
|||||||
- **🐛 Found a bug?** Open an [issue](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues) or fix it yourself!
|
- **🐛 Found a bug?** Open an [issue](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues) or fix it yourself!
|
||||||
- **💬 Need help?** Join our [Discord community](https://discord.gg/YzmbnCDkat)
|
- **💬 Need help?** Join our [Discord community](https://discord.gg/YzmbnCDkat)
|
||||||
|
|
||||||
We have lots of [`good first issue`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/good%20first%20issue) labels perfect for beginners!
|
We have lots of https://github.com/BetterSEQTA/BetterSEQTA-Plus/labels/good%20first%20issue labels perfect for beginners!
|
||||||
|
|
||||||
## Quick Development Setup
|
## Quick Development Setup
|
||||||
|
|
||||||
@@ -85,6 +85,8 @@ npm run dev
|
|||||||
2. Enable "Developer mode"
|
2. Enable "Developer mode"
|
||||||
3. Click "Load unpacked" → Select `dist` folder
|
3. Click "Load unpacked" → Select `dist` folder
|
||||||
4. Visit a SEQTA page to see it work! 🎉
|
4. Visit a SEQTA page to see it work! 🎉
|
||||||
|
> [!WARNING]
|
||||||
|
> Whenever you update the extension while not in dev mode, you will need to use the reload button on the extension page.
|
||||||
|
|
||||||
📚 **Need more details?** Check our [detailed setup guide](./docs/GETTING_STARTED_CONTRIBUTING.md#your-first-30-minutes)
|
📚 **Need more details?** Check our [detailed setup guide](./docs/GETTING_STARTED_CONTRIBUTING.md#your-first-30-minutes)
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,6 @@ Ready to contribute? Here's what to do next:
|
|||||||
Still confused about something? That's totally normal! Here are your options:
|
Still confused about something? That's totally normal! Here are your options:
|
||||||
- 💬 Ask in our [Discord server](https://discord.gg/YzmbnCDkat)
|
- 💬 Ask in our [Discord server](https://discord.gg/YzmbnCDkat)
|
||||||
- 🐛 Open an issue on GitHub
|
- 🐛 Open an issue on GitHub
|
||||||
- 📧 Email us at betterseqta@betterseqta.com
|
- 📧 Email us at betterseqta.plus@gmail.com
|
||||||
|
|
||||||
Remember: **Every expert was once a beginner!** We're here to help you learn and contribute. 🚀
|
Remember: **Every expert was once a beginner!** We're here to help you learn and contribute. 🚀
|
||||||
+12
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "betterseqtaplus",
|
"name": "betterseqtaplus",
|
||||||
"version": "3.4.9",
|
"version": "3.4.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
@@ -35,19 +35,19 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-runtime": "^7.26.9",
|
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||||
"@babel/runtime": "^7.26.9",
|
"@babel/runtime": "^7.26.9",
|
||||||
"@bedframe/cli": "^0.0.91",
|
"@bedframe/cli": "^0.0.95",
|
||||||
"@crxjs/vite-plugin": "2.0.0-beta.32",
|
"@crxjs/vite-plugin": "^2.2.0",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^10.0.0",
|
||||||
"dependency-cruiser": "^16.10.0",
|
"dependency-cruiser": "^17.0.1",
|
||||||
"eslint": "9.22.0",
|
"eslint": "^9.33.0",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^3.0.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"publish-browser-extension": "^3.0.0",
|
"publish-browser-extension": "^3.0.1",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.85.1",
|
||||||
"sass-loader": "^16.0.5",
|
"sass-loader": "^16.0.5",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"url": "^0.11.4"
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bedframe/core": "^0.0.46",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -65,10 +66,10 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/chrome": "^0.0.308",
|
"@types/chrome": "^0.1.4",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^4.2.0",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^24.3.0",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/webextension-polyfill": "^0.12.3",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
|
|||||||
+52
-8
@@ -38,6 +38,21 @@ body,
|
|||||||
html {
|
html {
|
||||||
font-family: Rubik, sans-serif !important;
|
font-family: Rubik, sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure native select dropdowns are readable on Windows */
|
||||||
|
select option {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
color: #111827 !important;
|
||||||
|
}
|
||||||
|
.dark select option {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consistent rounded corners for selects */
|
||||||
|
select {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
#container {
|
#container {
|
||||||
transition: 200ms;
|
transition: 200ms;
|
||||||
background: var(--auto-background) !important;
|
background: var(--auto-background) !important;
|
||||||
@@ -379,6 +394,18 @@ ul.magicDelete > li.deleting {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Allow long course/assessment names in the sidebar to wrap and break safely */
|
||||||
|
#menu li > label,
|
||||||
|
#menu section > label {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 16px;
|
||||||
|
hyphens: auto;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
#menu {
|
#menu {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
z-index: 19;
|
z-index: 19;
|
||||||
@@ -451,11 +478,6 @@ ul.magicDelete > li.deleting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu li > label,
|
|
||||||
#menu section > label {
|
|
||||||
text-transform: none;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
#userActions {
|
#userActions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -801,6 +823,11 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"],
|
||||||
|
html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"] [class*="ResourceList__name___ydvDT"] {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.assessmentsWrapper .message {
|
.assessmentsWrapper .message {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1111,7 +1138,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
height: 15em;
|
height: 15em;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: minmax(130px, 1fr);
|
grid-auto-columns: minmax(142px, 1fr);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
@@ -1320,7 +1347,17 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
font-size: 20px !important;
|
font-size: 20px !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
height: 36%;
|
/* Let the title expand naturally but clamp to 2 lines to avoid overlap */
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
.day h3 {
|
.day h3 {
|
||||||
padding: 0px 5px;
|
padding: 0px 5px;
|
||||||
@@ -1654,6 +1691,8 @@ iframe.userHTML {
|
|||||||
|
|
||||||
.programmeNavigator {
|
.programmeNavigator {
|
||||||
box-shadow: 0 0 40px 0px rgba(0,0,0,0.05);
|
box-shadow: 0 0 40px 0px rgba(0,0,0,0.05);
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.navigator {
|
.navigator {
|
||||||
padding: 6px !important;
|
padding: 6px !important;
|
||||||
@@ -1991,13 +2030,17 @@ div.liveEntry {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.dailycalMarker {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.uiFileHandler .uiButton {
|
.uiFileHandler .uiButton {
|
||||||
border-radius: 32px !important;
|
border-radius: 32px !important;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
margin-top: 4px !important;
|
margin-top: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.uiFile {
|
a.uiFile:not(.rows) {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
width: 200px !important;
|
width: 200px !important;
|
||||||
@@ -2021,6 +2064,7 @@ a.uiFile {
|
|||||||
svg {
|
svg {
|
||||||
color: white;
|
color: white;
|
||||||
position: unset !important;
|
position: unset !important;
|
||||||
|
display: unset !important;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
height: 42px !important;
|
height: 42px !important;
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ div:has(> #rbgcp-wrapper) {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#rbgcp-inputs-wrap {
|
||||||
|
padding-top: 4px !important;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
|
||||||
|
#rbgcp-hex-input,
|
||||||
|
#rbgcp-input {
|
||||||
|
height: 28px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
#rbgcp-wrapper {
|
#rbgcp-wrapper {
|
||||||
div[style="padding-top: 11px; position: relative;"] div {
|
div[style="padding-top: 11px; position: relative;"] div {
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export default function Picker({
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
disableDarkMode={true}
|
disableDarkMode={true}
|
||||||
presets={presets}
|
presets={presets}
|
||||||
hideInputs={customOnChange ? false : true}
|
|
||||||
value={customThemeColor ?? ""}
|
value={customThemeColor ?? ""}
|
||||||
onChange={(color: string) => {
|
onChange={(color: string) => {
|
||||||
if (customOnChange) {
|
if (customOnChange) {
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
let select: HTMLSelectElement;
|
let select: HTMLSelectElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-lg w-full overflow-clip">
|
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-xl w-full overflow-clip">
|
||||||
<select
|
<select
|
||||||
bind:this={select}
|
bind:this={select}
|
||||||
value={state}
|
value={state}
|
||||||
onchange={() => onChange(select.value)}
|
onchange={() => onChange(select.value)}
|
||||||
class="px-4 py-1 text-[0.75rem] dark:text-white w-full border-none bg-transparent focus:ring-0 focus:bg-white/20 dark:focus:bg-black/10"
|
class="px-4 py-2 pr-9 text-[0.875rem] font-medium text-black dark:text-white w-full border-none bg-white/80 dark:bg-zinc-800/70 hover:bg-white/90 dark:hover:bg-zinc-800/80 focus:bg-white/90 dark:focus:bg-zinc-800/80 focus:ring-0 rounded-md appearance-none transition-colors"
|
||||||
>
|
>
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
<option value={option.value}>
|
<option value={option.value}>
|
||||||
@@ -22,3 +22,19 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Make native dropdown list readable on Windows */
|
||||||
|
select option {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #111827; /* zinc-900 */
|
||||||
|
}
|
||||||
|
:global(.dark) select option {
|
||||||
|
background-color: #1f2937; /* zinc-800 */
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) div::after {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -183,18 +183,19 @@
|
|||||||
props: {
|
props: {
|
||||||
state: $settingsState.newsSource,
|
state: $settingsState.newsSource,
|
||||||
onChange: (value: string) => settingsState.newsSource = value,
|
onChange: (value: string) => settingsState.newsSource = value,
|
||||||
options: [
|
options: [
|
||||||
{ value: "australia", label: "Australia" },
|
{ value: "australia", label: "Australia" },
|
||||||
{ value: "usa", label: "USA" },
|
{ value: "usa", label: "USA" },
|
||||||
{ value: "taiwan", label: "Taiwan" },
|
{ value: "uk", label: "UK" },
|
||||||
{ value: "hong_kong", label: "Hong Kong" },
|
{ value: "taiwan", label: "Taiwan" },
|
||||||
{ value: "panama", label: "Panama" },
|
{ value: "hong_kong", label: "Hong Kong" },
|
||||||
{ value: "canada", label: "Canada" },
|
{ value: "panama", label: "Panama" },
|
||||||
{ value: "singapore", label: "Singapore" },
|
{ value: "canada", label: "Canada" },
|
||||||
{ value: "uk", label: "UK" },
|
{ value: "singapore", label: "Singapore" },
|
||||||
{ value: "japan", label: "Japan" },
|
{ value: "japan", label: "Japan" },
|
||||||
{ value: "netherlands", label: "Netherlands" }
|
{ value: "netherlands", label: "Netherlands" }
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
] as option}
|
] as option}
|
||||||
|
|||||||
@@ -23,13 +23,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const switchChange = (shortcut: any) => {
|
const switchChange = (shortcut: any) => {
|
||||||
const value = $settingsState.shortcuts.find(s => s.name === shortcut);
|
const idx = $settingsState.shortcuts.findIndex(s => s.name === shortcut);
|
||||||
if (value) {
|
if (idx !== -1) {
|
||||||
value.enabled = !value.enabled;
|
// Create a new array with the toggled value to ensure reactivity
|
||||||
settingsState.shortcuts = settingsState.shortcuts;
|
const updated = settingsState.shortcuts.map(s =>
|
||||||
|
s.name === shortcut ? { ...s, enabled: !s.enabled } : s
|
||||||
|
);
|
||||||
|
settingsState.shortcuts = updated;
|
||||||
} else {
|
} else {
|
||||||
settingsState.shortcuts = [...settingsState.shortcuts, { name: shortcut, enabled: true }];
|
settingsState.shortcuts = [
|
||||||
|
...settingsState.shortcuts,
|
||||||
|
{ name: shortcut, enabled: true }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,16 +202,6 @@
|
|||||||
</MotionDiv>
|
</MotionDiv>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each Object.entries(Shortcuts) as shortcut}
|
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
|
||||||
<div class="pr-4">
|
|
||||||
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
|
|
||||||
<h2 class="text-sm">{shortcut[1].DisplayName || shortcut[0]}</h2>
|
|
||||||
</div>
|
|
||||||
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<!-- Custom Shortcuts Section -->
|
<!-- Custom Shortcuts Section -->
|
||||||
{#each $settingsState.customshortcuts as shortcut, index}
|
{#each $settingsState.customshortcuts as shortcut, index}
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
@@ -217,6 +213,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#each Object.entries(Shortcuts) as shortcut}
|
||||||
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
|
<div class="pr-4">
|
||||||
|
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
|
||||||
|
<h2 class="text-sm">{shortcut[1].DisplayName || shortcut[0]}</h2>
|
||||||
|
</div>
|
||||||
|
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="p-4 text-center">
|
<div class="p-4 text-center">
|
||||||
Loading shortcuts...
|
Loading shortcuts...
|
||||||
|
|||||||
@@ -21,13 +21,16 @@
|
|||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<button
|
<button
|
||||||
onclick={() => editMode = !editMode}
|
onclick={() => editMode = !editMode}
|
||||||
class="absolute top-0 right-0 z-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{editMode ? '\ue9e4' : '\uec38'}</button>
|
class="absolute top-0 right-0 z-10 px-2 h-8 text-lg rounded-xl bg-zinc-100 dark:bg-zinc-700">
|
||||||
|
<span class="mr-2">{editMode ? 'Done' : 'Edit'}</span>
|
||||||
|
<span class="font-IconFamily">{editMode ? '\ue9e4' : '\uec38'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
|
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
|
||||||
<ThemeSelector isEditMode={editMode} />
|
<ThemeSelector isEditMode={editMode} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center w-full h-full">
|
<div class="flex justify-center items-center w-full h-full">
|
||||||
<div class="text-lg">
|
<div class="text-lg">
|
||||||
Open SEQTA and use the embedded settings to access theme settings. 🫠
|
Open SEQTA and use the embedded settings to access theme settings. 🫠
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import localforage from 'localforage'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
let fileInput = $state<HTMLInputElement | undefined>(undefined)
|
||||||
|
let dragging = $state(false)
|
||||||
|
let filename = $state<string | undefined>(undefined)
|
||||||
|
let durationText = $state<string | undefined>(undefined)
|
||||||
|
|
||||||
|
const store = localforage.createInstance({
|
||||||
|
name: 'background-music-store',
|
||||||
|
storeName: 'music',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadExisting() {
|
||||||
|
const name = await store.getItem<string>('audio-name')
|
||||||
|
filename = name ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => { loadExisting() })
|
||||||
|
|
||||||
|
function triggerSelect() { fileInput?.click() }
|
||||||
|
|
||||||
|
async function handleFiles(files: FileList | null) {
|
||||||
|
const file = files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
// Accept WAV and MP3 files
|
||||||
|
const isSupported = file.type === 'audio/wav' || file.type === 'audio/mpeg' ||
|
||||||
|
file.name.toLowerCase().endsWith('.wav') || file.name.toLowerCase().endsWith('.mp3')
|
||||||
|
if (!isSupported) {
|
||||||
|
alert('Please select a .wav or .mp3 audio file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.setItem('audio-blob', file)
|
||||||
|
await store.setItem('audio-name', file.name)
|
||||||
|
filename = file.name
|
||||||
|
|
||||||
|
// Probe duration
|
||||||
|
try {
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
const audio = new Audio(url)
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
audio.onloadedmetadata = () => resolve()
|
||||||
|
audio.onerror = () => reject()
|
||||||
|
})
|
||||||
|
if (!isNaN(audio.duration) && audio.duration !== Infinity) {
|
||||||
|
const minutes = Math.floor(audio.duration / 60)
|
||||||
|
const seconds = Math.round(audio.duration % 60)
|
||||||
|
durationText = `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||||
|
} else {
|
||||||
|
durationText = undefined
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch {
|
||||||
|
durationText = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('betterseqta-background-music-updated'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileChange() { handleFiles(fileInput?.files || null) }
|
||||||
|
|
||||||
|
function onDrop(event: DragEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
dragging = false
|
||||||
|
handleFiles(event.dataTransfer?.files || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAudio() {
|
||||||
|
await store.removeItem('audio-blob')
|
||||||
|
await store.removeItem('audio-name')
|
||||||
|
filename = undefined
|
||||||
|
durationText = undefined
|
||||||
|
window.dispatchEvent(new Event('betterseqta-background-music-stop'))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative cursor-pointer select-none"
|
||||||
|
onclick={() => triggerSelect()}
|
||||||
|
ondragover={(e) => { e.stopPropagation(); dragging = true }}
|
||||||
|
ondragleave={() => dragging = false}
|
||||||
|
ondrop={onDrop}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
triggerSelect()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div class="flex gap-3 items-center">
|
||||||
|
{#if filename}
|
||||||
|
<div class="flex items-center px-3 py-1 rounded-lg bg-zinc-200 dark:bg-zinc-800">
|
||||||
|
<div class="text-xs text-zinc-600 dark:text-zinc-300">
|
||||||
|
{filename}
|
||||||
|
<p>{durationText}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="flex justify-center items-center m-1 text-lg dark:text-white size-7"
|
||||||
|
onclick={(e) => { e.stopPropagation(); removeAudio() }}
|
||||||
|
aria-label="Remove audio"
|
||||||
|
>×</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex gap-2 items-center px-3 py-1 text-xs rounded-lg border border-dashed transition border-zinc-300 dark:border-zinc-600 text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 text-nowrap">
|
||||||
|
<span class="text-lg font-IconFamily">{'\ued47'}</span>
|
||||||
|
<span>Upload audio</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<input type="file" accept="audio/wav,audio/mpeg" class="hidden" bind:this={fileInput} onchange={onFileChange} />
|
||||||
|
{#if dragging}
|
||||||
|
<div class="absolute inset-0 rounded-lg bg-zinc-200/40 dark:bg-zinc-700/40"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import type { Plugin } from "@/plugins/core/types";
|
||||||
|
import { componentSetting, defineSettings, numberSetting, booleanSetting } from "@/plugins/core/settingsHelpers";
|
||||||
|
import styles from "./styles.css?inline";
|
||||||
|
import BackgroundMusicSetting from "./BackgroundMusicSetting.svelte";
|
||||||
|
import localforage from "localforage";
|
||||||
|
|
||||||
|
const settings = defineSettings({
|
||||||
|
uploader: componentSetting({
|
||||||
|
title: "Background Music",
|
||||||
|
description: "Upload a .wav or .mp3 audio file to play in the background.",
|
||||||
|
component: BackgroundMusicSetting,
|
||||||
|
}),
|
||||||
|
volume: numberSetting({
|
||||||
|
title: "Volume",
|
||||||
|
description: "Set background music volume",
|
||||||
|
default: 0.5,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.05,
|
||||||
|
}),
|
||||||
|
pauseOnHidden: booleanSetting({
|
||||||
|
title: "Pause when tab hidden",
|
||||||
|
description: "Pause music when switching to another tab or minimizing the browser",
|
||||||
|
default: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = localforage.createInstance({
|
||||||
|
name: "background-music-store",
|
||||||
|
storeName: "music",
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentAudio: HTMLAudioElement | null = null;
|
||||||
|
let currentObjectUrl: string | null = null;
|
||||||
|
let cleanupRegistered = false;
|
||||||
|
let pendingGestureCancel: (() => void) | null = null;
|
||||||
|
let visibilityResumeTimeout: number | null = null;
|
||||||
|
|
||||||
|
async function loadAudioBlob(): Promise<Blob | null> {
|
||||||
|
const blob = await store.getItem<Blob>("audio-blob");
|
||||||
|
return blob && blob instanceof Blob ? blob : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAndCleanupAudio(): void {
|
||||||
|
if (currentAudio) {
|
||||||
|
currentAudio.pause();
|
||||||
|
currentAudio.src = "";
|
||||||
|
currentAudio.remove();
|
||||||
|
currentAudio = null;
|
||||||
|
}
|
||||||
|
if (currentObjectUrl) {
|
||||||
|
URL.revokeObjectURL(currentObjectUrl);
|
||||||
|
currentObjectUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureGestureStart(handler: () => void): () => void {
|
||||||
|
const eventTypes = ["pointerdown", "keydown", "touchstart"]; // broad user gesture coverage
|
||||||
|
const listener = () => {
|
||||||
|
handler();
|
||||||
|
for (const type of eventTypes) {
|
||||||
|
window.removeEventListener(type, listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const type of eventTypes) {
|
||||||
|
window.addEventListener(type, listener, { once: true, passive: true });
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
for (const type of eventTypes) {
|
||||||
|
window.removeEventListener(type, listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startPlayback(volume: number): Promise<void> {
|
||||||
|
const blob = await loadAudioBlob();
|
||||||
|
if (!blob) return;
|
||||||
|
|
||||||
|
stopAndCleanupAudio();
|
||||||
|
|
||||||
|
currentObjectUrl = URL.createObjectURL(blob);
|
||||||
|
const audio = new Audio(currentObjectUrl);
|
||||||
|
audio.loop = true;
|
||||||
|
audio.volume = Math.max(0, Math.min(1, volume));
|
||||||
|
audio.preload = "auto";
|
||||||
|
audio.crossOrigin = "anonymous";
|
||||||
|
audio.style.display = "none";
|
||||||
|
document.body.appendChild(audio);
|
||||||
|
currentAudio = audio;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt immediate play; may be blocked until gesture
|
||||||
|
await audio.play();
|
||||||
|
} catch {
|
||||||
|
// Ignore; will be started after gesture if enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backgroundMusicPlugin: Plugin<typeof settings> = {
|
||||||
|
id: "background-music",
|
||||||
|
name: "Background Music",
|
||||||
|
description: "Play your own music in the background while SEQTA is open.",
|
||||||
|
version: "1.0.0",
|
||||||
|
settings,
|
||||||
|
styles,
|
||||||
|
disableToggle: true,
|
||||||
|
defaultEnabled: false,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
await api.storage.loaded;
|
||||||
|
|
||||||
|
// react to specific setting changes
|
||||||
|
api.settings.onChange("volume" as any, (value: any) => {
|
||||||
|
const vol = (typeof value === "number" ? value : 0.5) as number;
|
||||||
|
if (currentAudio) currentAudio.volume = Math.max(0, Math.min(1, vol));
|
||||||
|
});
|
||||||
|
|
||||||
|
api.settings.onChange("pauseOnHidden" as any, (value: any) => {
|
||||||
|
const pauseOnHidden = (typeof value === "boolean" ? value : true) as boolean;
|
||||||
|
// If the setting is disabled and audio is currently paused due to tab being hidden, resume it
|
||||||
|
if (!pauseOnHidden && currentAudio && currentAudio.paused && document.visibilityState === "hidden") {
|
||||||
|
currentAudio.play().catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Stop button/event removed by user; no stop handling needed
|
||||||
|
|
||||||
|
// Start if we have audio and autoplay is enabled
|
||||||
|
const tryStart = async () => {
|
||||||
|
const vol = (api.settings as any).volume ?? 0.5;
|
||||||
|
await startPlayback(vol);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always arm gesture start and attempt immediate start
|
||||||
|
const cancel = ensureGestureStart(() => { tryStart(); });
|
||||||
|
cleanupRegistered = true;
|
||||||
|
(window as any).__betterseqta_bg_music_cancel__ = cancel;
|
||||||
|
tryStart();
|
||||||
|
|
||||||
|
// Pause on tab hide, resume on show with a small delay (if enabled)
|
||||||
|
const visHandler = () => {
|
||||||
|
if (!currentAudio) return;
|
||||||
|
const pauseOnHidden = (api.settings as any).pauseOnHidden ?? true;
|
||||||
|
if (!pauseOnHidden) return;
|
||||||
|
|
||||||
|
if (document.visibilityState === "hidden") {
|
||||||
|
if (visibilityResumeTimeout !== null) {
|
||||||
|
clearTimeout(visibilityResumeTimeout);
|
||||||
|
visibilityResumeTimeout = null;
|
||||||
|
}
|
||||||
|
currentAudio.pause();
|
||||||
|
} else if (document.visibilityState === "visible") {
|
||||||
|
if (visibilityResumeTimeout !== null) {
|
||||||
|
clearTimeout(visibilityResumeTimeout);
|
||||||
|
}
|
||||||
|
visibilityResumeTimeout = window.setTimeout(() => {
|
||||||
|
visibilityResumeTimeout = null;
|
||||||
|
currentAudio?.play().catch(() => {});
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("visibilitychange", visHandler);
|
||||||
|
|
||||||
|
// Allow uploads to trigger refresh
|
||||||
|
const uploadedHandler = () => {
|
||||||
|
const vol = (api.settings as any).volume ?? 0.5;
|
||||||
|
startPlayback(vol);
|
||||||
|
};
|
||||||
|
window.addEventListener("betterseqta-background-music-updated", uploadedHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("visibilitychange", visHandler);
|
||||||
|
window.removeEventListener("betterseqta-background-music-updated", uploadedHandler);
|
||||||
|
if (cleanupRegistered && (window as any).__betterseqta_bg_music_cancel__) {
|
||||||
|
(window as any).__betterseqta_bg_music_cancel__();
|
||||||
|
(window as any).__betterseqta_bg_music_cancel__ = undefined;
|
||||||
|
}
|
||||||
|
if (pendingGestureCancel) { pendingGestureCancel(); pendingGestureCancel = null; }
|
||||||
|
if (visibilityResumeTimeout !== null) { clearTimeout(visibilityResumeTimeout); visibilityResumeTimeout = null; }
|
||||||
|
stopAndCleanupAudio();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default backgroundMusicPlugin;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.background-music-hidden{display:none}
|
||||||
|
|
||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
defineSettings,
|
defineSettings,
|
||||||
hotkeySetting,
|
hotkeySetting,
|
||||||
} from "../../core/settingsHelpers";
|
} from "../../core/settingsHelpers";
|
||||||
|
import styles from "./src/core/styles.css?inline";
|
||||||
|
|
||||||
// Platform-aware default hotkey
|
// Platform-aware default hotkey
|
||||||
const getDefaultHotkey = () => {
|
const getDefaultHotkey = () => {
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
@@ -82,6 +84,7 @@ export default defineLazyPlugin({
|
|||||||
disableToggle: true,
|
disableToggle: true,
|
||||||
defaultEnabled: false,
|
defaultEnabled: false,
|
||||||
beta: true,
|
beta: true,
|
||||||
|
styles: styles,
|
||||||
|
|
||||||
// Lazy loader - only imports the heavy plugin when actually needed
|
// Lazy loader - only imports the heavy plugin when actually needed
|
||||||
loader: () => import("./src/core/index")
|
loader: () => import("./src/core/index")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
|||||||
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
||||||
import profilePicturePlugin from "./built-in/profilePicture";
|
import profilePicturePlugin from "./built-in/profilePicture";
|
||||||
import assessmentsOverviewPlugin from "./built-in/assessmentsOverview";
|
import assessmentsOverviewPlugin from "./built-in/assessmentsOverview";
|
||||||
|
import backgroundMusicPlugin from "./built-in/backgroundMusic";
|
||||||
//import testPlugin from './built-in/test';
|
//import testPlugin from './built-in/test';
|
||||||
|
|
||||||
// Heavy plugins (lazy-loaded only when enabled)
|
// Heavy plugins (lazy-loaded only when enabled)
|
||||||
@@ -24,6 +25,7 @@ pluginManager.registerPlugin(notificationCollectorPlugin);
|
|||||||
pluginManager.registerPlugin(timetablePlugin);
|
pluginManager.registerPlugin(timetablePlugin);
|
||||||
pluginManager.registerPlugin(profilePicturePlugin);
|
pluginManager.registerPlugin(profilePicturePlugin);
|
||||||
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
||||||
|
pluginManager.registerPlugin(backgroundMusicPlugin);
|
||||||
//pluginManager.registerPlugin(testPlugin);
|
//pluginManager.registerPlugin(testPlugin);
|
||||||
|
|
||||||
// Register heavy plugins with lazy loading
|
// Register heavy plugins with lazy loading
|
||||||
|
|||||||
+2
-2
@@ -16,9 +16,9 @@ export async function main() {
|
|||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
injectPageState();
|
injectPageState();
|
||||||
|
|
||||||
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
||||||
if (import.meta.env.MODE === "development") {
|
if (import.meta.env.MODE === "development") {
|
||||||
import("../css/injected.scss");
|
import("@/css/injected.scss");
|
||||||
} else {
|
} else {
|
||||||
const injectedStyle = document.createElement("style");
|
const injectedStyle = document.createElement("style");
|
||||||
injectedStyle.textContent = injectedCSS;
|
injectedStyle.textContent = injectedCSS;
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ export function renderSettingsIfNeeded() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const shadow = extensionPopup.attachShadow({ mode: "open" });
|
const shadow = extensionPopup.attachShadow({ mode: "open" });
|
||||||
requestIdleCallback(() => renderSvelte(Settings, shadow));
|
if ('requestIdleCallback' in window) {
|
||||||
|
requestIdleCallback(() => renderSvelte(Settings, shadow));
|
||||||
|
} else {
|
||||||
|
renderSvelte(Settings, shadow);
|
||||||
|
}
|
||||||
isSettingsRendered = true;
|
isSettingsRendered = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export function addShortcuts(shortcuts: any) {
|
|||||||
|
|
||||||
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
||||||
// Creates the stucture and element information for each seperate shortcut
|
// Creates the stucture and element information for each seperate shortcut
|
||||||
|
const container = document.getElementById("shortcuts");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
let shortcut = document.createElement("a");
|
let shortcut = document.createElement("a");
|
||||||
shortcut.setAttribute("href", link);
|
shortcut.setAttribute("href", link);
|
||||||
shortcut.setAttribute("target", "_blank");
|
shortcut.setAttribute("target", "_blank");
|
||||||
@@ -42,5 +45,5 @@ function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
|||||||
shortcutdiv.append(text);
|
shortcutdiv.append(text);
|
||||||
shortcut.append(shortcutdiv);
|
shortcut.append(shortcutdiv);
|
||||||
|
|
||||||
document.getElementById("shortcuts")!.appendChild(shortcut);
|
container.appendChild(shortcut);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import stringToHTML from "../stringToHTML";
|
|||||||
|
|
||||||
export function CreateCustomShortcutDiv(element: any) {
|
export function CreateCustomShortcutDiv(element: any) {
|
||||||
// Creates the stucture and element information for each seperate shortcut
|
// Creates the stucture and element information for each seperate shortcut
|
||||||
|
const container = document.getElementById("shortcuts");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
var shortcut = document.createElement("a");
|
var shortcut = document.createElement("a");
|
||||||
shortcut.setAttribute("href", element.url);
|
shortcut.setAttribute("href", element.url);
|
||||||
shortcut.setAttribute("target", "_blank");
|
shortcut.setAttribute("target", "_blank");
|
||||||
@@ -45,5 +48,5 @@ export function CreateCustomShortcutDiv(element: any) {
|
|||||||
shortcutdiv.append(text);
|
shortcutdiv.append(text);
|
||||||
shortcut.append(shortcutdiv);
|
shortcut.append(shortcutdiv);
|
||||||
|
|
||||||
document.getElementById("shortcuts")!.append(shortcut);
|
container.append(shortcut);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import links from "@/seqta/content/links.json";
|
|
||||||
|
|
||||||
export function RemoveShortcutDiv(elements: any) {
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
elements.forEach((element: any) => {
|
|
||||||
const shortcuts = document.querySelectorAll(".shortcut");
|
|
||||||
shortcuts.forEach((shortcut) => {
|
|
||||||
const anchorElement = shortcut.parentElement; // the <a> element is the parent
|
|
||||||
const textElement = shortcut.querySelector("p"); // <p> is a direct child of .shortcut
|
|
||||||
const title = textElement ? textElement.textContent : "";
|
|
||||||
|
|
||||||
const elementName = links[element.name as keyof typeof links]?.DisplayName || element.name;
|
|
||||||
|
|
||||||
let shouldRemove = title === elementName;
|
|
||||||
|
|
||||||
// Check href only if element.url exists
|
|
||||||
if (element.url) {
|
|
||||||
shouldRemove =
|
|
||||||
shouldRemove && anchorElement!.getAttribute("href") === element.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRemove) {
|
|
||||||
anchorElement!.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,11 @@ import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
|
|||||||
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
|
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
|
||||||
import coursesicon from "@/seqta/icons/coursesIcon";
|
import coursesicon from "@/seqta/icons/coursesIcon";
|
||||||
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
||||||
import { addShortcuts } from "../Adders/AddShortcuts";
|
|
||||||
import { convertTo12HourFormat } from "../convertTo12HourFormat";
|
import { convertTo12HourFormat } from "../convertTo12HourFormat";
|
||||||
import { delay } from "../delay";
|
import { delay } from "../delay";
|
||||||
import { settingsState } from "../listeners/SettingsState";
|
import { settingsState } from "../listeners/SettingsState";
|
||||||
import stringToHTML from "../stringToHTML";
|
import stringToHTML from "../stringToHTML";
|
||||||
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
|
||||||
import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement";
|
import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement";
|
||||||
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
||||||
import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent";
|
import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent";
|
||||||
@@ -100,12 +99,7 @@ export async function loadHomePage() {
|
|||||||
|
|
||||||
const cleanup = setupTimetableListeners();
|
const cleanup = setupTimetableListeners();
|
||||||
|
|
||||||
try {
|
renderShortcuts();
|
||||||
addShortcuts(settingsState.shortcuts);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("[BetterSEQTA+] Error adding shortcuts:", err.message || err);
|
|
||||||
}
|
|
||||||
AddCustomShortcutsToPage();
|
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const TodayFormatted = formatDate(date);
|
const TodayFormatted = formatDate(date);
|
||||||
@@ -367,15 +361,6 @@ function comparedate(obj1: any, obj2: any) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AddCustomShortcutsToPage() {
|
|
||||||
let customshortcuts: any = settingsState.customshortcuts;
|
|
||||||
if (customshortcuts.length > 0) {
|
|
||||||
for (let i = 0; i < customshortcuts.length; i++) {
|
|
||||||
const element = customshortcuts[i];
|
|
||||||
CreateCustomShortcutDiv(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processNotices(response: any, labelArray: string[]) {
|
function processNotices(response: any, labelArray: string[]) {
|
||||||
const NoticeContainer = document.getElementById("notice-container");
|
const NoticeContainer = document.getElementById("notice-container");
|
||||||
@@ -1117,6 +1102,14 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FilterUpcomingAssessments(settingsState.subjectfilters);
|
FilterUpcomingAssessments(settingsState.subjectfilters);
|
||||||
|
|
||||||
|
if (assessments.length === 0) {
|
||||||
|
upcomingitemcontainer!.innerHTML = `
|
||||||
|
<div class="day-empty">
|
||||||
|
<img src="${browser.runtime.getURL(LogoLight)}" />
|
||||||
|
<p>No assessments available.</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
|
function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts";
|
||||||
|
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
||||||
|
|
||||||
|
export function renderShortcuts() {
|
||||||
|
const container = document.getElementById("shortcuts");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
addShortcuts(settingsState.shortcuts || []);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[BetterSEQTA+] Error adding built-in shortcuts:", err?.message || err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const custom = settingsState.customshortcuts || [];
|
||||||
|
for (const element of custom) {
|
||||||
|
try {
|
||||||
|
CreateCustomShortcutDiv(element);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[BetterSEQTA+] Error adding custom shortcut:", element?.name, err?.message || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -18,12 +18,27 @@ export async function SendNewsPage() {
|
|||||||
const main = document.getElementById("main");
|
const main = document.getElementById("main");
|
||||||
main!.innerHTML = "";
|
main!.innerHTML = "";
|
||||||
|
|
||||||
|
const displayCountry = (() => {
|
||||||
|
switch (settingsState.newsSource?.toLowerCase()) {
|
||||||
|
case "usa": return "the USA";
|
||||||
|
case "uk": return "the UK";
|
||||||
|
case "netherlands": return "the Netherlands";
|
||||||
|
default:
|
||||||
|
return settingsState.newsSource
|
||||||
|
? settingsState.newsSource
|
||||||
|
.split("_")
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ")
|
||||||
|
: "Australia";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const html = stringToHTML(/* html */ `
|
const html = stringToHTML(/* html */ `
|
||||||
<div class="home-root">
|
<div class="home-root">
|
||||||
<div class="home-container" id="news-container">
|
<div class="home-container" id="news-container">
|
||||||
<h1 class="border">Latest Headlines in ${settingsState.newsSource ? settingsState.newsSource.charAt(0).toUpperCase() + settingsState.newsSource.slice(1) : "Australia"}</h1>
|
<h1 class="border">Latest Headlines in ${displayCountry}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>`);
|
</div>`);
|
||||||
|
|
||||||
main!.append(html.firstChild!);
|
main!.append(html.firstChild!);
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,24 @@ export function OpenWhatsNewPopup() {
|
|||||||
|
|
||||||
let text = stringToHTML(/* html */ `
|
let text = stringToHTML(/* html */ `
|
||||||
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: scroll;">
|
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: scroll;">
|
||||||
|
|
||||||
|
<h1>3.4.11 - New Features & Bug Fixes</h1>
|
||||||
|
<li>Added Background Music plugin</li>
|
||||||
|
<li>Added empty state for assessments on homepage</li>
|
||||||
|
<li>Added Colour Picker hex/rgba controls</li>
|
||||||
|
<li>Fixed custom shortcuts positioning (moved above regular shortcuts)</li>
|
||||||
|
<li>Fixed Go to popup not scrolling properly</li>
|
||||||
|
<li>Made theme edit mode more plain</li>
|
||||||
|
<li>Other minor bug fixes and improvements</li>
|
||||||
|
|
||||||
|
<h1>3.4.10 - Minor bug fixes</h1>
|
||||||
|
<li>Fixed UI file styling incorrectly applying to documents</li>
|
||||||
|
<li>Fixed missing styles in global search</li>
|
||||||
|
<li>Added icons for image files in file viewer</li>
|
||||||
|
<li>Added rounded corners when dragging calendar events</li>
|
||||||
|
<li>Improved performance of element scanning</li>
|
||||||
|
<li>Other minor improvements</li>
|
||||||
|
|
||||||
<h1>3.4.9 - Bug Fixes and Performance Improvements</h1>
|
<h1>3.4.9 - Bug Fixes and Performance Improvements</h1>
|
||||||
<li>Fixed performance issues with large notices on the homepage</li>
|
<li>Fixed performance issues with large notices on the homepage</li>
|
||||||
<li>Improved performance when global search is disabled</li>
|
<li>Improved performance when global search is disabled</li>
|
||||||
|
|||||||
@@ -57,13 +57,37 @@ class EventManager {
|
|||||||
return { unregister };
|
return { unregister };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildSelector(options: EventListenerOptions): string | null {
|
||||||
|
if (options.textContent || options.customCheck) return null;
|
||||||
|
|
||||||
|
let selector = options.elementType || "";
|
||||||
|
if (options.id) {
|
||||||
|
selector += `#${CSS.escape(options.id)}`;
|
||||||
|
}
|
||||||
|
if (options.className) {
|
||||||
|
selector += `.${CSS.escape(options.className)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selector.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
private async scanExistingElements(
|
private async scanExistingElements(
|
||||||
options: EventListenerOptions,
|
options: EventListenerOptions,
|
||||||
callback: (element: Element) => void,
|
callback: (element: Element) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const root = options.parentElement || document.documentElement;
|
const root = options.parentElement || document.documentElement;
|
||||||
const elements = Array.from(root.getElementsByTagName("*"));
|
const selector = this.buildSelector(options);
|
||||||
elements.unshift(root);
|
let elements: Element[] = [];
|
||||||
|
|
||||||
|
if (selector) {
|
||||||
|
elements = Array.from(root.querySelectorAll(selector));
|
||||||
|
if (selector && root.matches && root.matches(selector)) {
|
||||||
|
elements.unshift(root);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements = Array.from(root.getElementsByTagName("*"));
|
||||||
|
elements.unshift(root);
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < elements.length; i += this.chunkSize) {
|
for (let i = 0; i < elements.length; i += this.chunkSize) {
|
||||||
const chunk = elements.slice(i, i + this.chunkSize);
|
const chunk = elements.slice(i, i + this.chunkSize);
|
||||||
|
|||||||
@@ -29,19 +29,11 @@ class StorageManager {
|
|||||||
},
|
},
|
||||||
set: (target, prop: keyof SettingsState, value) => {
|
set: (target, prop: keyof SettingsState, value) => {
|
||||||
const oldValue = target.data[prop];
|
const oldValue = target.data[prop];
|
||||||
|
|
||||||
// Only save if the value actually changed
|
// Only save if the reference actually changed
|
||||||
if (oldValue !== value) {
|
if (oldValue !== value) {
|
||||||
Reflect.set(target.data, prop, value);
|
Reflect.set(target.data, prop, value);
|
||||||
target.saveToStorage();
|
target.saveToStorage();
|
||||||
|
|
||||||
// Notify listeners immediately for responsiveness
|
|
||||||
const listeners = target.listeners.get(prop as string);
|
|
||||||
if (listeners) {
|
|
||||||
for (const listener of listeners) {
|
|
||||||
listener(value, oldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { settingsState } from "./SettingsState";
|
import { settingsState } from "./SettingsState";
|
||||||
import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
||||||
|
|
||||||
import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts";
|
// Shortcuts rendering
|
||||||
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
|
||||||
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
||||||
import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv";
|
|
||||||
|
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import type { CustomShortcut } from "@/types/storage";
|
import type { CustomShortcut } from "@/types/storage";
|
||||||
@@ -47,21 +46,7 @@ export class StorageChangeHandler {
|
|||||||
oldValue: CustomShortcut[],
|
oldValue: CustomShortcut[],
|
||||||
) {
|
) {
|
||||||
if (!newValue || !oldValue) return;
|
if (!newValue || !oldValue) return;
|
||||||
|
renderShortcuts();
|
||||||
if (newValue.length > oldValue.length) {
|
|
||||||
// New shortcut added - add the last one
|
|
||||||
CreateCustomShortcutDiv(newValue[oldValue.length]);
|
|
||||||
} else if (newValue.length < oldValue.length) {
|
|
||||||
// Shortcut removed - find which one was removed
|
|
||||||
const newSet = new Set(newValue.map(item => JSON.stringify(item)));
|
|
||||||
const removedElement = oldValue.find(
|
|
||||||
(oldItem) => !newSet.has(JSON.stringify(oldItem))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (removedElement) {
|
|
||||||
RemoveShortcutDiv([removedElement]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleShortcutsChange(
|
private handleShortcutsChange(
|
||||||
@@ -69,34 +54,7 @@ export class StorageChangeHandler {
|
|||||||
oldValue: { enabled: boolean; name: string }[],
|
oldValue: { enabled: boolean; name: string }[],
|
||||||
) {
|
) {
|
||||||
if (!newValue || !oldValue) return;
|
if (!newValue || !oldValue) return;
|
||||||
|
renderShortcuts();
|
||||||
// Create map for faster lookup
|
|
||||||
const oldMap = new Map(oldValue.map(item => [item.name, item.enabled]));
|
|
||||||
|
|
||||||
const addedShortcuts: { enabled: boolean; name: string }[] = [];
|
|
||||||
const removedShortcuts: { enabled: boolean; name: string }[] = [];
|
|
||||||
|
|
||||||
// Check for changes in shortcuts
|
|
||||||
for (const newItem of newValue) {
|
|
||||||
const oldEnabled = oldMap.get(newItem.name);
|
|
||||||
|
|
||||||
// Newly enabled shortcuts
|
|
||||||
if (newItem.enabled && (oldEnabled === undefined || !oldEnabled)) {
|
|
||||||
addedShortcuts.push(newItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Newly disabled shortcuts
|
|
||||||
if (!newItem.enabled && oldEnabled === true) {
|
|
||||||
removedShortcuts.push(newItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedShortcuts.length > 0) {
|
|
||||||
addShortcuts(addedShortcuts);
|
|
||||||
}
|
|
||||||
if (removedShortcuts.length > 0) {
|
|
||||||
RemoveShortcutDiv(removedShortcuts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTransparencyEffectsChange(newValue: boolean) {
|
private handleTransparencyEffectsChange(newValue: boolean) {
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export default defineConfig(({ command }) => ({
|
|||||||
settings: join(__dirname, "src", "interface", "index.html"),
|
settings: join(__dirname, "src", "interface", "index.html"),
|
||||||
pageState: join(__dirname, "src", "pageState.js"),
|
pageState: join(__dirname, "src", "pageState.js"),
|
||||||
},
|
},
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
if (warning.code === "FILE_NAME_CONFLICT") return;
|
||||||
|
warn(warning);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user