diff --git a/.eslintrc.json b/.eslintrc.json index b9fb23bc..f6cc10d1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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 diff --git a/README.md b/README.md index f9afa194..d85958e3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# SVELTE BUILD - NOT STABLE +Please don't use this in a production environment - it is quite buggy and is not fully completed as of yet.
+ +# + diff --git a/package.json b/package.json index aadca2d2..417ca1bd 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "name": "betterseqtaplus", - "version": "3.3.2", + "version": "3.4.0", "type": "module", "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development, while incorporating a plethora of new and improved features!", "browserslist": "> 0.5%, last 2 versions, not dead", "scripts": { - "dev": "MODE=chrome vite dev", - "dev:firefox": "MODE=firefox vite build --watch", - "build": "MODE=chrome vite build && MODE=firefox vite build", - "build:chrome": "MODE=chrome vite build", - "build:firefox": "MODE=firefox vite build", - "build:safari": "MODE=safari vite build", + "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", @@ -31,66 +31,66 @@ }, "license": "MIT", "devDependencies": { - "@crxjs/vite-plugin": "^2.0.0-beta.23", + "@crxjs/vite-plugin": "2.0.0-beta.25", "@types/mime-types": "^2.1.4", - "@vitejs/plugin-react-swc": "^3.6.0", - "eslint": "^8.56.0", + "@vitejs/plugin-react-swc": "^3.7.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", "glob": "^11.0.0", "mime-types": "^2.1.35", - "parcel": "^2.11.0", - "prettier": "^3.2.5", + "prettier": "^3.3.3", "process": "^0.11.10", - "sass": "^1.70.0", + "sass": "^1.78.0", "sass-loader": "^13.3.3", "semver": "^7.6.3", - "url": "^0.11.3" + "url": "^0.11.4" }, "dependencies": { "@bedframe/cli": "^0.0.85", - "@blocknote/core": "^0.14.1", - "@blocknote/mantine": "^0.14.1", - "@blocknote/react": "^0.14.1", + "@codemirror/lang-css": "^6.3.0", "@codemirror/lang-less": "^6.0.2", - "@heroicons/react": "^2.1.3", - "@million/lint": "latest", - "@tailwindcss/forms": "^0.5.7", + "@codemirror/theme-one-dark": "^6.1.2", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tsconfig/svelte": "^5.0.4", + "@types/chrome": "^0.0.270", "@types/color": "^3.0.6", "@types/dompurify": "^3.0.5", - "@types/lodash": "^4.17.4", - "@types/node": "^20.11.30", - "@types/react": "^18.2.55", - "@types/react-dom": "^18.2.19", - "@types/sortablejs": "^1.15.7", + "@types/lodash": "^4.17.7", + "@types/node": "^20.16.5", + "@types/react": "17", + "@types/react-dom": "17", + "@types/sortablejs": "^1.15.8", "@types/uuid": "^9.0.8", "@types/webextension-polyfill": "^0.10.7", - "@uiw/codemirror-extensions-color": "^4.21.25", - "@uiw/codemirror-theme-github": "^4.21.25", - "@uiw/react-codemirror": "^4.21.25", - "autoprefixer": "^10.4.17", + "@uiw/codemirror-extensions-color": "^4.23.3", + "@uiw/codemirror-theme-github": "^4.23.3", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", "classnames": "^2.5.1", + "codemirror": "^6.0.1", "color": "^4.2.3", - "dompurify": "^3.0.8", - "framer-motion": "^11.0.25", + "dompurify": "^3.1.6", + "embla-carousel-autoplay": "^8.3.1", + "embla-carousel-svelte": "^8.3.1", + "fuse.js": "^7.0.0", + "idb": "^8.0.0", "kolorist": "^1.8.0", "localforage": "^1.10.0", "lodash": "^4.17.21", - "million": "latest", - "motion": "^10.17.0", + "million": "^3.1.11", + "motion": "^10.18.0", + "postcss": "^8.4.45", "publish-browser-extension": "^2.2.1", - "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", + "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.4.2", + "vite": "^5.4.4", "webextension-polyfill": "^0.10.0" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 4fbc9251..b4f84912 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -35,6 +35,10 @@ import coursesicon from '@/seqta/icons/coursesIcon' import iframeCSS from '@/css/iframe.scss?raw' import injectedCSS from '@/css/injected.scss?inline' import documentLoadCSS from '@/css/documentload.scss?inline' +import renderSvelte from '@/interface/main' +import Settings from '@/interface/pages/settings.svelte' +import { settingsPopup } from './interface/hooks/SettingsPopup' +import { migrateBackgrounds } from './seqta/utils/migrateBackgrounds' let SettingsClicked = false export let MenuOptionsOpen = false @@ -42,10 +46,13 @@ let currentSelectedDate = new Date() let LessonInterval: any var IsSEQTAPage = false +let hasSEQTAText = false // This check is placed outside of the document load event due to issues with EP (https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/84) -const hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software') -init() +if (document.childNodes[1]) { + hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software') ?? false + init() +} async function init() { CheckForMenuList() @@ -153,6 +160,15 @@ export function OpenWhatsNewPopup() { let text = stringToHTML( /* html */ `
+

3.4.0 - Major Performance Update

+
  • Completely rebuilt the extension popup using Svelte for dramatically improved performance
  • +
  • Added a brand new background store with search functionality and downloadable backgrounds
  • +
  • Significant code cleanup and optimization across the extension
  • +
  • Improved overall responsiveness and load times
  • +
  • Smoother animations and improved scrolling
  • +
  • Fixed Firefox compatibility issues
  • +
  • Other minor bug fixes and under the hood improvements
  • +

    3.3.1 - Hot Fix

  • Fixed assessments not loading when no notices are available
  • @@ -342,6 +358,102 @@ export function OpenWhatsNewPopup() { }) } +export function OpenAboutPage() { + const background = document.createElement('div') + background.id = 'whatsnewbk' + background.classList.add('whatsnewBackground') + + const container = document.createElement('div') + container.classList.add('whatsnewContainer') + + var header: any = stringToHTML( + /* html */ + `
    +

    About

    +

    BetterSEQTA+ V${browser.runtime.getManifest().version}

    +
    ` + ).firstChild + + let text = stringToHTML( + /* html */ ` +
    + + +

    BetterSEQTA+ is a fork of BetterSEQTA which was originally developed by Nulkem, which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features.

    +

    We are currently working on fixing bugs and adding good features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below).

    +

    Credits

    +

    Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph and SethBurkart123.

    +
    + `, + ).firstChild + + let footer = stringToHTML( + /* html */ ` +
    +
    + Report bugs and feedback: + + + + + + + + + + +
    +
    + `).firstChild + + let exitbutton = document.createElement('div') + exitbutton.id = 'whatsnewclosebutton' + + container.append(header) + container.append(text as ChildNode) + container.append(footer as ChildNode) + container.append(exitbutton) + + background.append(container) + + document.getElementById('container')!.append(background) + + let bkelement = document.getElementById('whatsnewbk') + let popup = document.getElementsByClassName('whatsnewContainer')[0] + + if (settingsState.animations) { + animate( + [popup, bkelement as HTMLElement], + { scale: [0, 1], opacity: [0, 1] }, + { easing: spring({ stiffness: 220, damping: 18 }) } + ) + + animate( + '.whatsnewTextContainer *', + { opacity: [0, 1], y: [10, 0] }, + { + delay: stagger(0.05, { start: 0.1 }), + duration: 0.5, + easing: [.22, .03, .26, 1] + } + ) + } + + delete settingsState.justupdated + + bkelement!.addEventListener('click', function (event) { + // Check if the click event originated from the element itself and not any of its children + if (event.target === bkelement) { + DeleteWhatsNew() + } + }); + + var closeelement = document.getElementById('whatsnewclosebutton') + closeelement!.addEventListener('click', function () { + DeleteWhatsNew() + }) +} + export async function finishLoad() { try { document.querySelector('.legacy-root')?.classList.remove('hidden'); @@ -356,7 +468,10 @@ export async function finishLoad() { if (settingsState.justupdated && !document.getElementById('whatsnewbk')) { OpenWhatsNewPopup(); + + /* Background Migration script */ } + migrateBackgrounds(); } async function DeleteWhatsNew() { @@ -522,12 +637,9 @@ function applyDarkModeToIframe(iframe: HTMLIFrameElement, cssLink: HTMLStyleElem const iframeDocument = iframe.contentDocument; if (!iframeDocument) return; - if (iframeDocument.readyState !== 'complete') { - iframe.onload = () => { - applyDarkModeToIframe(iframe, cssLink); - }; - return; - } + iframe.onload = () => { + applyDarkModeToIframe(iframe, cssLink); + }; if (settingsState.DarkMode) { iframeDocument.documentElement.classList.add('dark') @@ -935,30 +1047,22 @@ export function AppendElementsToDisabledPage() { document.head.append(settingsStyle) } -export function closeSettings() { - const ExtensionSettings = document.getElementById('ExtensionPopup')! - const ExtensionIframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement +export const closeExtensionPopup = (extensionPopup?: HTMLElement) => { + if (!extensionPopup) extensionPopup = document.getElementById('ExtensionPopup')! - if (SettingsClicked == true) { - ExtensionSettings!.classList.add('hide') - if (settingsState.animations) { - animate( - '#ExtensionPopup', - { opacity: [1, 0], scale: [1, 0] }, - { easing: spring({ stiffness: 220, damping: 18 }) } - ) - } else { - ExtensionSettings.style.opacity = '0' - ExtensionSettings.style.transform = 'scale(0)' - } - SettingsClicked = false - - if (ExtensionIframe.contentWindow) { - ExtensionIframe.contentWindow.postMessage('popupClosed', '*') - } + extensionPopup.classList.add('hide') + if (settingsState.animations) { + animate((progress) => { + extensionPopup.style.opacity = Math.max(0, 1 - progress).toString() + extensionPopup.style.transform = `scale(${Math.max(0, 1 - progress)})` + }, { easing: spring({ stiffness: 520, damping: 20 }) }) + } else { + extensionPopup.style.opacity = '0' + extensionPopup.style.transform = 'scale(0)' } - - ExtensionSettings!.classList.add('hide') + + settingsPopup.triggerClose() + SettingsClicked = false } export function addExtensionSettings() { @@ -969,42 +1073,23 @@ export function addExtensionSettings() { const extensionContainer = document.querySelector('#container') as HTMLDivElement if (extensionContainer) extensionContainer.appendChild(extensionPopup) - const extensionIframe: HTMLIFrameElement = document.createElement('iframe') - extensionIframe.src = `${browser.runtime.getURL('interface/index.html')}#settings/embedded` - extensionIframe.id = 'ExtensionIframe' - extensionIframe.setAttribute('allowTransparency', 'true') - extensionIframe.setAttribute('excludeDarkCheck', 'true') - extensionIframe.style.width = '384px' - extensionIframe.style.height = '100%' - extensionIframe.style.border = 'none' - extensionPopup.appendChild(extensionIframe) + // create shadow dom and render svelte app + try { + const shadow = extensionPopup.attachShadow({ mode: 'open' }); + requestIdleCallback(() => renderSvelte(Settings, shadow)); + } catch (err) { + console.error(err) + } const container = document.getElementById('container') new SettingsResizer(); - - const closeExtensionPopup = () => { - const ExtensionIframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement - - extensionPopup.classList.add('hide') - if (settingsState.animations) { - animate( - '#ExtensionPopup', - { opacity: [1, 0], scale: [1, 0] }, - { easing: spring({ stiffness: 220, damping: 18 }) } - ) - } else { - extensionPopup.style.opacity = '0' - extensionPopup.style.transform = 'scale(0)' - } - if (ExtensionIframe.contentWindow) { - ExtensionIframe.contentWindow.postMessage('popupClosed', '*') - } - SettingsClicked = false - } container!.onclick = (event) => { - if ((event.target as HTMLElement).closest('#AddedSettings') == null && SettingsClicked) { + if (!SettingsClicked) return; + + if (!(event.target as HTMLElement).closest('#AddedSettings')) { + if (event.target == extensionPopup) return; closeExtensionPopup() } } @@ -1239,21 +1324,17 @@ export function setupSettingsButton() { var AddedSettings = document.getElementById('AddedSettings'); var extensionPopup = document.getElementById('ExtensionPopup'); - AddedSettings!.addEventListener('click', function () { + AddedSettings!.addEventListener('click', async () => { if (SettingsClicked) { - extensionPopup!.classList.add('hide'); - if (settingsState.animations) { - animate('#ExtensionPopup', { opacity: [1, 0], scale: [1, 0] }, { easing: spring({ stiffness: 220, damping: 18 }) }); - } else { - extensionPopup!.style.opacity = '0' - extensionPopup!.style.transform = 'scale(0)' - } - (document.getElementById('ExtensionIframe')! as HTMLIFrameElement).contentWindow!.postMessage('popupClosed', '*'); - SettingsClicked = false; + closeExtensionPopup(extensionPopup as HTMLElement); } else { - extensionPopup!.classList.remove('hide'); if (settingsState.animations) { - animate('#ExtensionPopup', { opacity: [0, 1], scale: [0, 1] }, { easing: spring({ stiffness: 260, damping: 24 }) }); + animate((progress) => { + extensionPopup!.style.opacity = progress.toString() + extensionPopup!.style.transform = `scale(${progress})` + }, { easing: spring({ stiffness: 280, damping: 20 }) }); + + extensionPopup!.classList.remove('hide'); } else { extensionPopup!.style.opacity = '1' extensionPopup!.style.transform = 'scale(1)' diff --git a/src/background.ts b/src/background.ts index 4c41dffe..e95640ce 100644 --- a/src/background.ts +++ b/src/background.ts @@ -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) => { diff --git a/src/css/injected.scss b/src/css/injected.scss index 6153625c..3dca2557 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -1,9 +1,9 @@ @charset "UTF-8"; -@import url('https://fonts.googleapis.com/css?family=Rubik:300,400,500,600'); +@import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600"); -@import './injected/sidebar-animation.scss'; -@import './injected/theme.scss'; -@import './injected/transparency.scss'; +@import "./injected/sidebar-animation.scss"; +@import "./injected/theme.scss"; +@import "./injected/transparency.scss"; :root { background: var(--better-main) !important; @@ -16,7 +16,12 @@ } body, -.legacy-root input, .legacy-root textarea, .legacy-root button, .legacy-root select, .legacy-root option, .legacy-root .input, +.legacy-root input, +.legacy-root textarea, +.legacy-root button, +.legacy-root select, +.legacy-root option, +.legacy-root .input, html { font-family: Rubik, sans-serif !important; } @@ -26,10 +31,6 @@ html { } * { --theme-fg-parts: white; - - transition: - background-color 200ms ease-in-out, - backdrop-filter 200ms ease-in-out; } .extension-editor { background: var(--background-primary); @@ -39,7 +40,8 @@ html { height: 100%; visibility: visible !important; } -#themeCreatorIframe { + +#themeCreator { position: fixed; right: 0; height: 100%; @@ -62,6 +64,40 @@ html { } } +#store { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 9998; + animation: fadeIn 500ms forwards; + animation-delay: 200ms; + opacity: 0; + + &.hide { + animation: fadeOut 500ms forwards; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + .dark .resizeBar { background-color: rgb(28 28 30); } @@ -169,7 +205,8 @@ html { background: unset; } -.legacy-root button:active, .legacy-root a:active:not(.cke_combo_button) { +.legacy-root button:active, +.legacy-root a:active:not(.cke_combo_button) { background-image: unset !important; } @@ -179,7 +216,8 @@ html { } .dark .dashboard section { - input, select { + input, + select { background: rgba(255, 255, 255, 0.1); } } @@ -201,7 +239,7 @@ html { input, select { border: transparent; - background: rgba(0, 0, 0, .1); + background: rgba(0, 0, 0, 0.1); color: var(--text-primary); } @@ -243,7 +281,7 @@ html { color: var(--text-primary); } ul.magicDelete > li:hover { - background: rgba(0, 0, 0, .1); + background: rgba(0, 0, 0, 0.1); } .dashlet-notes > .editor { background: unset; @@ -261,7 +299,17 @@ ul.magicDelete > li.deleting { background: transparent !important; color: var(--text-color) !important; } +#media-container { + width: 100%; + height: 100%; +} +#media-container video, +#media-container img { + width: 100%; + height: 100%; + object-fit: cover; +} #menu li, #menu section { margin: 8px auto !important; @@ -292,7 +340,6 @@ ul.magicDelete > li.deleting { #menu { width: 270px; z-index: 19; - transition-duration: 0.4s; background: var(--better-main) !important; color: var(--text-color); border-right: none; @@ -338,14 +385,16 @@ ul.magicDelete > li.deleting { position: relative; &::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; bottom: 0; width: 3px; background: var(--item-colour, transparent); - transition: width 100ms, transform 0.3s ease; + transition: + width 100ms, + transform 0.3s ease; border-radius: 8px 0 0 8px; } @@ -447,7 +496,7 @@ ol:has(.MessageList__avatar___2wxyb svg) { .quickbar .actions [title="Choose a colour"] > svg { scale: 0.9; } -.quickbar[data-yiq='light'] .actions { +.quickbar[data-yiq="light"] .actions { color: white !important; } .singleSelect > li { @@ -474,7 +523,7 @@ ol:has(.MessageList__avatar___2wxyb svg) { } #main .timetablepage .quickbar { border: none; - box-shadow: 0 4px 8px rgba(0,0,0,0.5); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); border-radius: 16px; } .quickbar .actions { @@ -505,7 +554,7 @@ ol:has(.MessageList__avatar___2wxyb svg) { margin: 0 0 0 -12px; background-color: rgba(255, 255, 255, 0.2); clip-path: polygon(50% 40%, 0 0, 100% 0); - border: 12px solid rgba(255,255,255,0); + border: 12px solid rgba(255, 255, 255, 0); border-top-color: transparent; } #main > .timetablepage > .quickbar.above::before { @@ -1294,7 +1343,9 @@ div > ol:has(.uiFileHandlerWrapper) { height: 25px; width: 24px; } -.notifications__notifications___3mmLY > button > .notifications__bubble___1EkSQ { +.notifications__notifications___3mmLY + > button + > .notifications__bubble___1EkSQ { background: var(--better-alert-highlight); width: 25px; height: 25px; @@ -1396,7 +1447,7 @@ div > ol:has(.uiFileHandlerWrapper) { font-size: 12px; margin-right: 4px; font-family: "IconFamily"; - pointer-events: none; + pointer-events: none; } .MessageList__MessageList___3DxoC ol .Button__Button___3SRFo { @@ -1500,7 +1551,9 @@ iframe.userHTML { .Collapsible__Collapsible___3O8P3 > .Collapsible__header___-Afvq { background: none; } -.AssessmentList__AssessmentList___1GdCl > .AssessmentList__searchFilter___3N70o + .AssessmentList__items___3LcmQ { +.AssessmentList__AssessmentList___1GdCl + > .AssessmentList__searchFilter___3N70o + + .AssessmentList__items___3LcmQ { color: var(--text-primary); } .Thermoscore__Thermoscore___2tWMi { @@ -1550,19 +1603,10 @@ iframe.userHTML { #main > .course > .content > .resources > h2 { color: var(--text-primary); } -::-webkit-scrollbar { - width: 10px; - height: 10px; - transition: 1s; - border-radius: 16px; - //width: 0px !important; - //background: none; -} -::-webkit-scrollbar-track { - background-color: transparent; -} -::-webkit-scrollbar-corner { - background: none; + +/* set button(top and bottom of the scrollbar) */ +body::-webkit-scrollbar-button { + display: none !important; } :root, html, @@ -1571,8 +1615,19 @@ div, ol, ul { scrollbar-width: thin !important; - scrollbar-color: var(--better-light) var(--better-sub); + scrollbar-color: #babac0 #fff !important; } + +.dark { + body, + div, + ol, + ul { + scrollbar-width: thin !important; + scrollbar-color: #333 #111 !important; + } +} + .connectedNotificationsWrapper > div > button { color: var(--text-primary) !important; height: 45px; @@ -1596,10 +1651,13 @@ ul { > .SelectedAssessment__clearBtn___21D85 { background: var(--better-main); } -.SelectedAssessment__SelectedAssessment___3Bu5D > .SelectedAssessment__meta___1gq_y { +.SelectedAssessment__SelectedAssessment___3Bu5D + > .SelectedAssessment__meta___1gq_y { border-bottom: 1px solid var(--better-main); } -.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk > li.TabSet__selected___1psfF { +.TabSet__TabSet___Vo-SZ + > ol.TabSet__tabs___1RRZk + > li.TabSet__selected___1psfF { border-bottom-color: var(--better-main); } .TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk { @@ -1724,7 +1782,15 @@ div.entry.class[style*="left: 46.5%"] { div.entry.class[style*="width: 46.5%"] { width: 50% !important; } -.timetablepage .dailycal > .content > .wrapper > .days > tbody > tr > td > .entriesWrapper { +.timetablepage + .dailycal + > .content + > .wrapper + > .days + > tbody + > tr + > td + > .entriesWrapper { min-width: 0; width: auto !important; } @@ -1918,7 +1984,6 @@ div.bar.flat { .cke_toolbox > .cke_toolbar .cke_button_on { background-color: #3d3d3e !important; } - } .legacy-root input.singleSelect:focus { background: var(--auto-background); @@ -1962,7 +2027,15 @@ body { .forumView .assessment { background: var(--better-main); } -.dailycal > .content > .wrapper > .days > tbody > tr > td > .entriesWrapper > .entry { +.dailycal + > .content + > .wrapper + > .days + > tbody + > tr + > td + > .entriesWrapper + > .entry { padding: 3px; } .Viewer__Viewer___32BH- { @@ -2001,7 +2074,9 @@ li.MessageList__unread___3imtO { border-radius: 1600px; } -.MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz.MessageList__unread___3imtO { +.MessageList__MessageList___3DxoC + > ol + > li.MessageList__selected___1SJNz.MessageList__unread___3imtO { box-shadow: none; } @@ -2021,7 +2096,9 @@ li.MessageList__unread___3imtO { transition: width 0.1s; } -.MessageList__MessageList___3DxoC > ol > li.MessageList__unread___3imtO::before { +.MessageList__MessageList___3DxoC + > ol + > li.MessageList__unread___3imtO::before { width: 3px; } .connectedNotificationsWrapper > div > button { @@ -2095,7 +2172,10 @@ li.MessageList__unread___3imtO { cursor: pointer; } -.dark .MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz { +.dark + .MessageList__MessageList___3DxoC + > ol + > li.MessageList__selected___1SJNz { background: var(--background-secondary); } @@ -2513,20 +2593,21 @@ li.MessageList__unread___3imtO { border-radius: 5px; color: var(--text-color); } -/* On mouse-over, add a grey background color */ + .upcoming-checkbox-container:hover input ~ .upcoming-checkmark { filter: brightness(0.8); } -/* When the checkbox is checked, add a blue background */ + .upcoming-checkbox-container input:checked ~ .upcoming-checkmark { background: var(--item-colour); } -/* Create the checkmark/indicator (hidden when not checked) */ + .upcoming-checkmark:after { content: ""; position: absolute; display: none; } + /* Show the checkmark when checked */ .upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after { display: block; @@ -2585,7 +2666,9 @@ li.MessageList__unread___3imtO { width: 100%; display: flex; flex-direction: column; - box-shadow: inset 0px 6px 0 var(--item-colour, transparent), inset 0px 40px 50px -40px rgba(179, 179, 179, 0.9); + box-shadow: + inset 0px 6px 0 var(--item-colour, transparent), + inset 0px 40px 50px -40px rgba(179, 179, 179, 0.9); transition: 200ms; position: relative; height: 15em; @@ -2594,7 +2677,9 @@ li.MessageList__unread___3imtO { font-family: Rubik, sans-serif; } .dark .day { - box-shadow: inset 0px 6px 0 var(--item-colour, transparent), inset 0px 40px 50px -40px rgba(0,0,0,0.9); + box-shadow: + inset 0px 6px 0 var(--item-colour, transparent), + inset 0px 40px 50px -40px rgba(0, 0, 0, 0.9); } .clickable { cursor: pointer; @@ -2619,6 +2704,157 @@ li.MessageList__unread___3imtO { margin: 0; } +.upcoming-items { + background: var(--background-primary); + width: 100%; + max-height: 55em; + overflow-y: auto; + display: flex; + flex-direction: column; + color: var(--text-primary); + transition: 200ms; + border-radius: 16px; +} +.dark .upcoming-items { + box-shadow: inset 0px 40px 80px -40px rgba(0, 0, 0, 0.6); +} +.upcoming-assessment-title { + color: var(--text-primary); + transition: 200ms; + font-size: 10px; + margin: 0; +} +.upcoming-assessment { + border: 3px solid var(--item-colour); + margin: 5px 50px; + height: 6em; + padding: 0px; + border-radius: 10px; +} +.upcoming-assessment { + display: flex; +} +.upcoming-date-container { + margin-bottom: 20px; +} +.upcoming-date-title h5 { + margin: 0; + font-weight: 500; +} +.upcoming-details { + width: 60%; + display: flex; + flex-direction: column; + justify-content: center; + padding: 0px 12px; +} +.upcoming-details h5 { + text-transform: uppercase; + color: #aaaaaa; + padding: 0px 4px; + margin: 0; +} +.upcoming-details p { + font-size: 15px; + padding: 4px; +} +.upcoming-special-day { + font-size: 20px; +} +.upcoming-blank { + display: flex; + border-bottom: 2px solid #bebebe; + margin: 5px 50px; + height: 2em; + padding: 0px; +} +.upcoming-blank p { + padding: 0; + margin: 0; +} +.upcoming-title { + display: flex; + align-content: space-between; +} +.upcoming-filters { + display: flex; + height: 26px; + width: 65%; + align-self: center; + align-items: center; + color: var(--text-color); + padding: 5px; + overflow-x: scroll; + overflow-y: hidden; +} +.upcoming-checkbox-container { + position: relative; + padding: none !important; + padding-left: 25px !important; + padding-right: 10px !important; + cursor: pointer; + font-size: 12px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + height: 20px; + align-items: center; + display: flex; +} +/* Hide the browser's default checkbox */ +.upcoming-checkbox-container input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + padding: 0; +} +/* Create a custom checkbox */ +.upcoming-checkmark { + position: absolute; + top: 0; + left: 0; + height: 15px; + width: 15px; + border: 3px solid var(--item-colour); + 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; +} +/* Style the checkmark/indicator */ +.upcoming-checkbox-container .upcoming-checkmark:after { + left: 3.5px; + top: 0px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} +.upcoming-hiddenassessment { + color: #797979; +} .titlebar { align-items: center; transition: 200ms; @@ -2760,7 +2996,6 @@ li.MessageList__unread___3imtO { width: 90%; border-radius: 16px; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3); - } .whatsnewTextContainer { display: flex; @@ -2830,9 +3065,20 @@ li.MessageList__unread___3imtO { } .whatsnewTextContainer h1:not(.whatsnewTextHeader) { position: sticky; + font-size: 1.2em; width: 100%; top: 0; background: var(--background-primary) !important; z-index: 1; - padding: 10px; + padding: 12px; + padding-left: 0px; + padding-bottom: 8px; } + +.whatsnewTextContainer img { + width: 100%; + border-radius: 12px; + aspect-ratio: 16/9; + object-fit: cover; + margin-bottom: 12px; +} \ No newline at end of file diff --git a/src/css/injected/popup.scss b/src/css/injected/popup.scss index f2ae0ef5..2f040bfe 100644 --- a/src/css/injected/popup.scss +++ b/src/css/injected/popup.scss @@ -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; } \ No newline at end of file diff --git a/src/interface/SettingsContext.tsx b/src/interface/SettingsContext.tsx deleted file mode 100644 index ae78ce7d..00000000 --- a/src/interface/SettingsContext.tsx +++ /dev/null @@ -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>; - showPicker: boolean; - setShowPicker: React.Dispatch>; - standalone: boolean; - setStandalone: React.Dispatch>; -} | undefined>(undefined); - -export const SettingsContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [settingsState, setSettingsState] = useState({ - 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(false); - const [standalone, setStandalone] = useState(false); - - useSettingsState({ settingsState, setSettingsState }); - - return ( - - {children} - - ); -}; - -// 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; -}; diff --git a/src/interface/assets/presetBackgrounds.tsx b/src/interface/assets/presetBackgrounds.tsx deleted file mode 100644 index d6258c0d..00000000 --- a/src/interface/assets/presetBackgrounds.tsx +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/interface/assets/react.svg b/src/interface/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/src/interface/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/interface/components/Accordian.tsx b/src/interface/components/Accordian.tsx deleted file mode 100644 index 0fef18e7..00000000 --- a/src/interface/components/Accordian.tsx +++ /dev/null @@ -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(null); - const [shown, setShown] = useState(false); - - useEffect(() => { - const show = async () => { - if (defaultOpened) { - await new Promise(resolve => setTimeout(resolve, 100)); - setShown(true); - } - }; - - show(); - }, []) - - return ( -
    - -
    - {children} -
    -
    - ); -}; - -export default Accordion; diff --git a/src/interface/components/BackgroundSelector.css b/src/interface/components/BackgroundSelector.css deleted file mode 100644 index 58a82a77..00000000 --- a/src/interface/components/BackgroundSelector.css +++ /dev/null @@ -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; -} diff --git a/src/interface/components/BackgroundSelector.tsx b/src/interface/components/BackgroundSelector.tsx deleted file mode 100644 index aaeb3607..00000000 --- a/src/interface/components/BackgroundSelector.tsx +++ /dev/null @@ -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([]); - const [selectedBackground, setSelectedBackground] = useState(); - const [downloadedPresetIds, setDownloadedPresetIds] = useState([]); - const [downloadProgress, setDownloadProgress] = useState>({}); - - const [BackgroundsBlocked, setBackgroundsBlocked] = useState(false); - - useEffect(() => { - GetTheme().then((theme) => { - setSelectedBackground(theme); - }); - }, []); - - const handleFileChange = async (e: ChangeEvent): Promise => { - 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 => { - 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 => { - 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 => { - 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 ( - <> - - - {BackgroundsBlocked && ( -
    -

    File Storage Blocked

    -

    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)

    - Brave browser logo -
    - )} - -
    -

    Background Images

    -
    - { isEditMode ? <> : -
    -
    - {/* Plus icon */} -  -
    - -
    } - {backgrounds.filter(bg => bg.type === 'image').map(bg => ( -
    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 && ( -
    deleteBackground(bg.id)}> -
    -
    - )} - swatch -
    - ))} - {backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'image' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => ( - - ))} -
    - -

    Background Videos

    -
    - { isEditMode ? <> : -
    -
    - {/* Plus icon */} -  -
    - -
    - } - {backgrounds.filter(bg => bg.type === 'video').map(bg => ( -
    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 && ( -
    deleteBackground(bg.id)}> -
    -
    - )} -
    - ))} - {backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'video' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => ( -
    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 && ( -
    - - - - -
    - )} -
    - - {downloadProgress[bg.id] === undefined ? '' : ''} - -
    -
    - ))} -
    -
    - - ); -} - -export default memo(BackgroundSelector); \ No newline at end of file diff --git a/src/interface/components/Button.svelte b/src/interface/components/Button.svelte new file mode 100644 index 00000000..97f07499 --- /dev/null +++ b/src/interface/components/Button.svelte @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/src/interface/components/Checkbox.tsx b/src/interface/components/Checkbox.tsx deleted file mode 100644 index 421f4640..00000000 --- a/src/interface/components/Checkbox.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; - -type CheckboxProps = { - value: boolean; - onChange: (e: React.ChangeEvent) => void; -}; - -const Checkbox: React.FC = ({ value, onChange }) => { - return ( -
    - -
    -
    Description (optional)
    - +
    + + + +
    +
    + {'\uec60'} +
    + {theme.coverImage ? 'Change' : 'Add'} cover image + + {#if !theme.hideThemeName && theme.coverImage} +
    {theme.name}
    + {/if} + {#if theme.coverImage} +
    + Cover + {/if} +
    + + + + {#each [ + { + type: 'switch', + title: 'Hide Theme Name', + description: 'Useful when your cover image contains text', + props: { + state: theme.hideThemeName, + onChange: (value: boolean) => theme = { ...theme, hideThemeName: value } + } + }, + { + type: 'switch', + title: 'Force Theme', + description: 'Force users to use either dark or light mode', + props: { + state: theme.forceDark !== undefined, + onChange: (value: boolean) => theme = { ...theme, forceDark: value ? false : undefined } + } + }, + { + type: 'conditional', + props: { + condition: theme.forceDark !== undefined, + children: { + type: 'lightDarkToggle', + title: 'Mode', + description: 'Choose whether to force light or dark mode', + props: { + state: theme.forceDark === true, + onChange: (value: boolean) => theme = { ...theme, forceDark: value } + } + } + } + }, + { + type: 'colourPicker', + title: 'Default Theme Colour', + description: 'Set the default color for your theme', + direction: 'vertical', + props: { + customState: theme.defaultColour, + customOnChange: (color: string) => theme = { ...theme, defaultColour: color } + } + }, + { + type: 'imageUpload', + title: 'Custom Images', + description: 'Add custom images to your theme', + direction: 'vertical', + }, + { + type: 'codeEditor', + title: 'Custom CSS', + description: 'Add custom CSS to your theme', + direction: 'vertical', + props: { + value: theme.CustomCSS, + onChange: (value: string) => { theme = { ...theme, CustomCSS: value } } + } + } + ] as SettingItem[] as setting} + {@render settingItem(setting)} + {/each} + + + + \ No newline at end of file diff --git a/src/interface/types/AppProps.ts b/src/interface/types/AppProps.ts deleted file mode 100644 index fad51e0c..00000000 --- a/src/interface/types/AppProps.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface SettingsState { - notificationCollector: boolean; - selectedTheme: string; - lessonAlerts: boolean; - animatedBackground: boolean; - animatedBackgroundSpeed: string; - customThemeColor: string; - betterSEQTAPlus: boolean; - shortcuts: Shortcut[]; - customshortcuts: CustomShortcut[]; - transparencyEffects: boolean; - timeFormat?: string; - animations: boolean; - defaultPage: string; - devMode: boolean; -} - -interface Shortcut { - enabled: boolean; - name: string; -} - -export interface CustomShortcut { - name: string; - url: string; - icon: string; -} \ No newline at end of file diff --git a/src/interface/types/ColorPickerProps.ts b/src/interface/types/ColorPickerProps.ts deleted file mode 100644 index ae888971..00000000 --- a/src/interface/types/ColorPickerProps.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ColorPickerProps { - color: string; - onChange: (color: string) => void; - id: string; -} \ No newline at end of file diff --git a/src/interface/types/SettingsProps.ts b/src/interface/types/SettingsProps.ts index f3961ae3..bb20acc5 100644 --- a/src/interface/types/SettingsProps.ts +++ b/src/interface/types/SettingsProps.ts @@ -1,11 +1,7 @@ -import type { SettingsState } from './AppProps'; - export interface SettingsList { title: string; + id: number; description: string; - modifyElement: JSX.Element; -} -export interface SettingsProps { - settingsState: SettingsState; - setSettingsState: React.Dispatch>; -} + Component: any; /* TODO: Give this a type */ + props?: any; +} \ No newline at end of file diff --git a/src/interface/types/SliderProps.ts b/src/interface/types/SliderProps.ts deleted file mode 100644 index 7ae50459..00000000 --- a/src/interface/types/SliderProps.ts +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import "./Slider.css"; -export interface Slider { - onValueChange: (value: number) => void; -} -declare const Slider: React.FC; -export default Slider; diff --git a/src/interface/types/SwitchProps.ts b/src/interface/types/SwitchProps.ts deleted file mode 100644 index 8a43742d..00000000 --- a/src/interface/types/SwitchProps.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "./Switch.css"; - -export interface SwitchProps { - onChange: (isOn: boolean) => void; - state: boolean; -} \ No newline at end of file diff --git a/src/interface/types/TabbedContainerProps.ts b/src/interface/types/TabbedContainerProps.ts deleted file mode 100644 index 801a9afc..00000000 --- a/src/interface/types/TabbedContainerProps.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React, { JSX } from 'react'; -export interface Tab { - title: string; - content: JSX.Element; -} -export interface TabbedContainerProps { - tabs: Tab[]; - animations?: boolean; -} -declare const TabbedContainer: React.FC; -export default TabbedContainer; diff --git a/src/interface/types/Theme.ts b/src/interface/types/Theme.ts new file mode 100644 index 00000000..8ebe4b51 --- /dev/null +++ b/src/interface/types/Theme.ts @@ -0,0 +1,7 @@ +export type Theme = { + name: string; + description: string; + coverImage: string; + marqueeImage: string; + id: string; +}; \ No newline at end of file diff --git a/src/interface/types/pocketbase-types.ts b/src/interface/types/pocketbase-types.ts deleted file mode 100644 index f044c91f..00000000 --- a/src/interface/types/pocketbase-types.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** -* This file was @generated using pocketbase-typegen -*/ - -import type PocketBase from 'pocketbase' -import type { RecordService } from 'pocketbase' - -export enum Collections { - PublishedThemes = "publishedThemes", - Themes = "themes", - Users = "users", -} - -// Alias types for improved usability -export type IsoDateString = string -export type RecordIdString = string -export type HTMLString = string - -// System fields -export type BaseSystemFields = { - id: RecordIdString - created: IsoDateString - updated: IsoDateString - collectionId: string - collectionName: Collections - expand?: T -} - -export type AuthSystemFields = { - email: string - emailVisibility: boolean - username: string - verified: boolean -} & BaseSystemFields - -// Record types for each collection - -export type PublishedThemesRecord = { - coverImage?: string - description?: string - downloads?: string - marqueeImage?: string - name: string - themeURL?: string -} - -export type ThemesRecord = { - coverImage?: string - description?: string - downloads?: string - images?: string[] - name: string - submitted?: boolean - theme?: null | Ttheme -} - -export type UsersRecord = { - avatar?: string - name?: string -} - -// Response types include system fields and match responses from the PocketBase API -export type PublishedThemesResponse = Required & BaseSystemFields -export type ThemesResponse = Required> & BaseSystemFields -export type UsersResponse = Required & AuthSystemFields - -// Types containing all Records and Responses, useful for creating typing helper functions - -export type CollectionRecords = { - publishedThemes: PublishedThemesRecord - themes: ThemesRecord - users: UsersRecord -} - -export type CollectionResponses = { - publishedThemes: PublishedThemesResponse - themes: ThemesResponse - users: UsersResponse -} - -// Type for usage with type asserted PocketBase instance -// https://github.com/pocketbase/js-sdk#specify-typescript-definitions - -export type TypedPocketBase = PocketBase & { - collection(idOrName: 'publishedThemes'): RecordService - collection(idOrName: 'themes'): RecordService - collection(idOrName: 'users'): RecordService -} diff --git a/src/interface/utils/standalone.svelte.ts b/src/interface/utils/standalone.svelte.ts new file mode 100644 index 00000000..b5274a7e --- /dev/null +++ b/src/interface/utils/standalone.svelte.ts @@ -0,0 +1,36 @@ +import type { Subscriber, Unsubscriber } from "svelte/store"; + +export class Standalone { + private static instance: Standalone; + private _standalone = $state(false); + private subscribers = new Set>(); + + private constructor() {} + + public static getInstance(): Standalone { + if (!Standalone.instance) { + Standalone.instance = new Standalone(); + } + return Standalone.instance; + } + + public setStandalone(value: boolean) { + this._standalone = value; + this.subscribers.forEach(subscriber => subscriber(value)); + } + + public get standalone() { + return this._standalone; + } + + public subscribe(run: Subscriber): Unsubscriber { + this.subscribers.add(run); + run(this._standalone); + + return () => { + this.subscribers.delete(run); + }; + } +} + +export const standalone = Standalone.getInstance(); \ No newline at end of file diff --git a/src/interface/utils/themeImageHandlers.ts b/src/interface/utils/themeImageHandlers.ts new file mode 100644 index 00000000..977d02ff --- /dev/null +++ b/src/interface/utils/themeImageHandlers.ts @@ -0,0 +1,60 @@ +import type { LoadedCustomTheme } from '@/types/CustomThemes'; + +export function generateImageId(): string { + return Math.random().toString(36).substr(2, 9); +} + +export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promise | LoadedCustomTheme { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ''; + if (file) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = async () => { + const imageBlob = await fetch(reader.result as string).then(res => res.blob()); + const imageId = generateImageId(); + const variableName = `custom-image-${theme.CustomImages.length}`; + resolve({ + ...theme, + CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: URL.createObjectURL(imageBlob) }], + }); + }; + reader.readAsDataURL(file); + }); + } + return theme; +} + +export function handleRemoveImage(imageId: string, theme: LoadedCustomTheme): LoadedCustomTheme { + return { + ...theme, + CustomImages: theme.CustomImages.filter((image) => image.id !== imageId), + } as LoadedCustomTheme; +} + +export function handleImageVariableChange(imageId: string, variableName: string, theme: LoadedCustomTheme): LoadedCustomTheme { + return { + ...theme, + CustomImages: theme.CustomImages.map((image) => + image.id === imageId ? { ...image, variableName } : image + ), + } as LoadedCustomTheme; +} + +export function handleCoverImageUpload(event: Event, theme: LoadedCustomTheme): Promise { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ''; + if (file) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = async () => { + const imageBlob = await fetch(reader.result as string).then(res => res.blob()); + resolve({ ...theme, coverImage: imageBlob, coverImageUrl: URL.createObjectURL(imageBlob) }); + }; + reader.readAsDataURL(file); + }); + } + return Promise.resolve(theme); +} \ No newline at end of file diff --git a/src/interface/vite-env.d.ts b/src/interface/vite-env.d.ts deleted file mode 100644 index 11f02fe2..00000000 --- a/src/interface/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/src/manifests/manifest.json b/src/manifests/manifest.json index 857fe1a6..72cdf633 100644 --- a/src/manifests/manifest.json +++ b/src/manifests/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "BetterSEQTA+", - "version": "3.3.1", + "version": "3.4.0", "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!", "icons": { "32": "resources/icons/icon-32.png", @@ -23,7 +23,7 @@ "service_worker": "background.ts" }, "content_security_policy": { - "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + "extension_pages": "script-src 'self'; object-src 'self'" }, "content_scripts": [ { @@ -33,17 +33,13 @@ } ], "web_accessible_resources": [ - { - "resources": ["interface/index.html"], - "matches": ["*://*/*"] - }, - { - "resources": ["seqta/ui/background/background.html"], - "matches": ["*://*/*"] - }, { "resources": ["*://*/*"], "matches": ["*://*/*"] + }, + { + "resources": ["resources/icons/*"], + "matches": ["*://*/*"] } ] } diff --git a/src/resources/branding/dark.jpg b/src/resources/branding/dark.jpg new file mode 100644 index 00000000..a7631ac9 Binary files /dev/null and b/src/resources/branding/dark.jpg differ diff --git a/src/resources/branding/light.jpg b/src/resources/branding/light.jpg new file mode 100644 index 00000000..74255e1b Binary files /dev/null and b/src/resources/branding/light.jpg differ diff --git a/src/resources/fonts/demo-files/demo.css b/src/resources/fonts/demo-files/demo.css new file mode 100644 index 00000000..163ca059 --- /dev/null +++ b/src/resources/fonts/demo-files/demo.css @@ -0,0 +1,152 @@ +body { + padding: 0; + margin: 0; + font-family: sans-serif; + font-size: 1em; + line-height: 1.5; + color: #555; + background: #fff; +} +h1 { + font-size: 1.5em; + font-weight: normal; +} +small { + font-size: .66666667em; +} +a { + color: #e74c3c; + text-decoration: none; +} +a:hover, a:focus { + box-shadow: 0 1px #e74c3c; +} +.bshadow0, input { + box-shadow: inset 0 -2px #e7e7e7; +} +input:hover { + box-shadow: inset 0 -2px #ccc; +} +input, fieldset { + font-family: sans-serif; + font-size: 1em; + margin: 0; + padding: 0; + border: 0; +} +input { + color: inherit; + line-height: 1.5; + height: 1.5em; + padding: .25em 0; +} +input:focus { + outline: none; + box-shadow: inset 0 -2px #449fdb; +} +.glyph { + font-size: 16px; + width: 15em; + padding-bottom: 1em; + margin-right: 4em; + margin-bottom: 1em; + float: left; + overflow: hidden; +} +.liga { + width: 80%; + width: calc(100% - 2.5em); +} +.talign-right { + text-align: right; +} +.talign-center { + text-align: center; +} +.bgc1 { + background: #f1f1f1; +} +.fgc1 { + color: #999; +} +.fgc0 { + color: #000; +} +p { + margin-top: 1em; + margin-bottom: 1em; +} +.mvm { + margin-top: .75em; + margin-bottom: .75em; +} +.mtn { + margin-top: 0; +} +.mtl, .mal { + margin-top: 1.5em; +} +.mbl, .mal { + margin-bottom: 1.5em; +} +.mal, .mhl { + margin-left: 1.5em; + margin-right: 1.5em; +} +.mhmm { + margin-left: 1em; + margin-right: 1em; +} +.mls { + margin-left: .25em; +} +.ptl { + padding-top: 1.5em; +} +.pbs, .pvs { + padding-bottom: .25em; +} +.pvs, .pts { + padding-top: .25em; +} +.unit { + float: left; +} +.unitRight { + float: right; +} +.size1of2 { + width: 50%; +} +.size1of1 { + width: 100%; +} +.clearfix:before, .clearfix:after { + content: " "; + display: table; +} +.clearfix:after { + clear: both; +} +.hidden-true { + display: none; +} +.textbox0 { + width: 3em; + background: #f1f1f1; + padding: .25em .5em; + line-height: 1.5; + height: 1.5em; +} +#testDrive { + display: block; + padding-top: 24px; + line-height: 1.5; +} +.fs0 { + font-size: 16px; +} +.fs1 { + font-size: 16px; +} + diff --git a/src/resources/fonts/demo-files/demo.js b/src/resources/fonts/demo-files/demo.js new file mode 100644 index 00000000..6f45f1c4 --- /dev/null +++ b/src/resources/fonts/demo-files/demo.js @@ -0,0 +1,30 @@ +if (!('boxShadow' in document.body.style)) { + document.body.setAttribute('class', 'noBoxShadow'); +} + +document.body.addEventListener("click", function(e) { + var target = e.target; + if (target.tagName === "INPUT" && + target.getAttribute('class').indexOf('liga') === -1) { + target.select(); + } +}); + +(function() { + var fontSize = document.getElementById('fontSize'), + testDrive = document.getElementById('testDrive'), + testText = document.getElementById('testText'); + function updateTest() { + testDrive.innerHTML = testText.value || String.fromCharCode(160); + if (window.icomoonLiga) { + window.icomoonLiga(testDrive); + } + } + function updateSize() { + testDrive.style.fontSize = fontSize.value + 'px'; + } + fontSize.addEventListener('change', updateSize, false); + testText.addEventListener('input', updateTest, false); + testText.addEventListener('change', updateTest, false); + updateSize(); +}()); diff --git a/src/resources/fonts/style.css b/src/resources/fonts/style.css new file mode 100644 index 00000000..0078ed19 --- /dev/null +++ b/src/resources/fonts/style.css @@ -0,0 +1,3549 @@ +@font-face { + font-family: 'icomoon'; + src: url('fonts/icomoon.eot?biv4go'); + src: url('fonts/icomoon.eot?biv4go#iefix') format('embedded-opentype'), + url('IconFamily.woff') format('woff'), + url('fonts/icomoon.svg?biv4go#icomoon') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +input.unitRight { + font-family: 'icomoon'; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-activity-heart:before { + content: "\e900"; +} +.icon-activity:before { + content: "\e901"; +} +.icon-airplay:before { + content: "\e902"; +} +.icon-airpods:before { + content: "\e903"; +} +.icon-alarm-clock-check:before { + content: "\e904"; +} +.icon-alarm-clock-minus:before { + content: "\e905"; +} +.icon-alarm-clock-off:before { + content: "\e906"; +} +.icon-alarm-clock-plus:before { + content: "\e907"; +} +.icon-alarm-clock:before { + content: "\e908"; +} +.icon-alert-circle:before { + content: "\e909"; +} +.icon-alert-hexagon:before { + content: "\e90a"; +} +.icon-alert-octagon:before { + content: "\e90b"; +} +.icon-alert-square:before { + content: "\e90c"; +} +.icon-alert-triangle:before { + content: "\e90d"; +} +.icon-align-bottom-01:before { + content: "\e90e"; +} +.icon-align-bottom-02:before { + content: "\e90f"; +} +.icon-align-center:before { + content: "\e910"; +} +.icon-align-horizontal-centre-01:before { + content: "\e911"; +} +.icon-align-horizontal-centre-02:before { + content: "\e912"; +} +.icon-align-justify:before { + content: "\e913"; +} +.icon-align-left-01:before { + content: "\e914"; +} +.icon-align-left-02:before { + content: "\e915"; +} +.icon-align-left:before { + content: "\e916"; +} +.icon-align-right-01:before { + content: "\e917"; +} +.icon-align-right-02:before { + content: "\e918"; +} +.icon-align-right:before { + content: "\e919"; +} +.icon-align-top-arrow-01:before { + content: "\e91a"; +} +.icon-align-top-arrow-02:before { + content: "\e91b"; +} +.icon-align-vertical-center-01:before { + content: "\e91c"; +} +.icon-align-vertical-center-02:before { + content: "\e91d"; +} +.icon-anchor:before { + content: "\e91e"; +} +.icon-annotation-alert:before { + content: "\e91f"; +} +.icon-annotation-check:before { + content: "\e920"; +} +.icon-annotation-dots:before { + content: "\e921"; +} +.icon-annotation-heart:before { + content: "\e922"; +} +.icon-annotation-info:before { + content: "\e923"; +} +.icon-annotation-plus:before { + content: "\e924"; +} +.icon-annotation-question:before { + content: "\e925"; +} +.icon-annotation-x:before { + content: "\e926"; +} +.icon-annotation:before { + content: "\e927"; +} +.icon-announcement-01:before { + content: "\e928"; +} +.icon-announcement-02:before { + content: "\e929"; +} +.icon-announcement-03:before { + content: "\e92a"; +} +.icon-archive:before { + content: "\e92b"; +} +.icon-arrow-block-down:before { + content: "\e92c"; +} +.icon-arrow-block-left:before { + content: "\e92d"; +} +.icon-arrow-block-right:before { + content: "\e92e"; +} +.icon-arrow-block-up:before { + content: "\e92f"; +} +.icon-arrow-circle-broken-down-left:before { + content: "\e930"; +} +.icon-arrow-circle-broken-down-right:before { + content: "\e931"; +} +.icon-arrow-circle-broken-down:before { + content: "\e932"; +} +.icon-arrow-circle-broken-left:before { + content: "\e933"; +} +.icon-arrow-circle-broken-right:before { + content: "\e934"; +} +.icon-arrow-circle-broken-up-left:before { + content: "\e935"; +} +.icon-arrow-circle-broken-up-right:before { + content: "\e936"; +} +.icon-arrow-circle-broken-up:before { + content: "\e937"; +} +.icon-arrow-circle-down-left:before { + content: "\e938"; +} +.icon-arrow-circle-down-right:before { + content: "\e939"; +} +.icon-arrow-circle-down:before { + content: "\e93a"; +} +.icon-arrow-circle-left:before { + content: "\e93b"; +} +.icon-arrow-circle-right:before { + content: "\e93c"; +} +.icon-arrow-circle-up-left:before { + content: "\e93d"; +} +.icon-arrow-circle-up-right:before { + content: "\e93e"; +} +.icon-arrow-circle-up:before { + content: "\e93f"; +} +.icon-arrow-down-left:before { + content: "\e940"; +} +.icon-arrow-down-right:before { + content: "\e941"; +} +.icon-arrow-down:before { + content: "\e942"; +} +.icon-arrow-left:before { + content: "\e943"; +} +.icon-arrow-narrow-down-left:before { + content: "\e944"; +} +.icon-arrow-narrow-down-right:before { + content: "\e945"; +} +.icon-arrow-narrow-down:before { + content: "\e946"; +} +.icon-arrow-narrow-left:before { + content: "\e947"; +} +.icon-arrow-narrow-right:before { + content: "\e948"; +} +.icon-arrow-narrow-up-left:before { + content: "\e949"; +} +.icon-arrow-narrow-up-right:before { + content: "\e94a"; +} +.icon-arrow-narrow-up:before { + content: "\e94b"; +} +.icon-arrow-right:before { + content: "\e94c"; +} +.icon-arrow-square-down-left:before { + content: "\e94d"; +} +.icon-arrow-square-down-right:before { + content: "\e94e"; +} +.icon-arrow-square-down:before { + content: "\e94f"; +} +.icon-arrow-square-left:before { + content: "\e950"; +} +.icon-arrow-square-right:before { + content: "\e951"; +} +.icon-arrow-square-up-left:before { + content: "\e952"; +} +.icon-arrow-square-up-right:before { + content: "\e953"; +} +.icon-arrow-square-up:before { + content: "\e954"; +} +.icon-arrow-up-left:before { + content: "\e955"; +} +.icon-arrow-up-right:before { + content: "\e956"; +} +.icon-arrow-up:before { + content: "\e957"; +} +.icon-arrows-down:before { + content: "\e958"; +} +.icon-arrows-left:before { + content: "\e959"; +} +.icon-arrows-right:before { + content: "\e95a"; +} +.icon-arrows-triangle:before { + content: "\e95b"; +} +.icon-arrows-up:before { + content: "\e95c"; +} +.icon-asterisk-01:before { + content: "\e95d"; +} +.icon-asterisk-02:before { + content: "\e95e"; +} +.icon-at-sign:before { + content: "\e95f"; +} +.icon-atom-01:before { + content: "\e960"; +} +.icon-atom-02:before { + content: "\e961"; +} +.icon-attachment-01:before { + content: "\e962"; +} +.icon-attachment-02:before { + content: "\e963"; +} +.icon-award-01:before { + content: "\e964"; +} +.icon-award-02:before { + content: "\e965"; +} +.icon-award-03:before { + content: "\e966"; +} +.icon-award-04:before { + content: "\e967"; +} +.icon-award-05:before { + content: "\e968"; +} +.icon-backpack:before { + content: "\e969"; +} +.icon-bank-note-01:before { + content: "\e96a"; +} +.icon-bank-note-02:before { + content: "\e96b"; +} +.icon-bank-note-03:before { + content: "\e96c"; +} +.icon-bank:before { + content: "\e96d"; +} +.icon-bar-chart-01:before { + content: "\e96e"; +} +.icon-bar-chart-02:before { + content: "\e96f"; +} +.icon-bar-chart-03:before { + content: "\e970"; +} +.icon-bar-chart-04:before { + content: "\e971"; +} +.icon-bar-chart-05:before { + content: "\e972"; +} +.icon-bar-chart-06:before { + content: "\e973"; +} +.icon-bar-chart-07:before { + content: "\e974"; +} +.icon-bar-chart-08:before { + content: "\e975"; +} +.icon-bar-chart-09:before { + content: "\e976"; +} +.icon-bar-chart-10:before { + content: "\e977"; +} +.icon-bar-chart-11:before { + content: "\e978"; +} +.icon-bar-chart-12:before { + content: "\e979"; +} +.icon-bar-chart-circle-01:before { + content: "\e97a"; +} +.icon-bar-chart-circle-02:before { + content: "\e97b"; +} +.icon-bar-chart-circle-03:before { + content: "\e97c"; +} +.icon-bar-chart-square-01:before { + content: "\e97d"; +} +.icon-bar-chart-square-02:before { + content: "\e97e"; +} +.icon-bar-chart-square-03:before { + content: "\e97f"; +} +.icon-bar-chart-square-down:before { + content: "\e980"; +} +.icon-bar-chart-square-minus:before { + content: "\e981"; +} +.icon-bar-chart-square-plus:before { + content: "\e982"; +} +.icon-bar-chart-square-up:before { + content: "\e983"; +} +.icon-bar-line-chart:before { + content: "\e984"; +} +.icon-battery-charging-01:before { + content: "\e985"; +} +.icon-battery-charging-02:before { + content: "\e986"; +} +.icon-battery-empty:before { + content: "\e987"; +} +.icon-battery-full:before { + content: "\e988"; +} +.icon-battery-low:before { + content: "\e989"; +} +.icon-battery-mid:before { + content: "\e98a"; +} +.icon-beaker-01:before { + content: "\e98b"; +} +.icon-beaker-02:before { + content: "\e98c"; +} +.icon-bell-01:before { + content: "\e98d"; +} +.icon-bell-02:before { + content: "\e98e"; +} +.icon-bell-03:before { + content: "\e98f"; +} +.icon-bell-04:before { + content: "\e990"; +} +.icon-bell-minus:before { + content: "\e991"; +} +.icon-bell-off-01:before { + content: "\e992"; +} +.icon-bell-off-02:before { + content: "\e993"; +} +.icon-bell-off-03:before { + content: "\e994"; +} +.icon-bell-plus:before { + content: "\e995"; +} +.icon-bell-ringing-01:before { + content: "\e996"; +} +.icon-bell-ringing-02:before { + content: "\e997"; +} +.icon-bell-ringing-03:before { + content: "\e998"; +} +.icon-bell-ringing-04:before { + content: "\e999"; +} +.icon-bezier-curve-01:before { + content: "\e99a"; +} +.icon-bezier-curve-02:before { + content: "\e99b"; +} +.icon-bezier-curve-03:before { + content: "\e99c"; +} +.icon-bluetooth-connect:before { + content: "\e99d"; +} +.icon-bluetooth-off:before { + content: "\e99e"; +} +.icon-bluetooth-on:before { + content: "\e99f"; +} +.icon-bluetooth-signal:before { + content: "\e9a0"; +} +.icon-bold-01:before { + content: "\e9a1"; +} +.icon-bold-02:before { + content: "\e9a2"; +} +.icon-bold-square:before { + content: "\e9a3"; +} +.icon-book-closed:before { + content: "\e9a4"; +} +.icon-book-open-01:before { + content: "\e9a5"; +} +.icon-book-open-02:before { + content: "\e9a6"; +} +.icon-bookmark-add:before { + content: "\e9a7"; +} +.icon-bookmark-check:before { + content: "\e9a8"; +} +.icon-bookmark-minus:before { + content: "\e9a9"; +} +.icon-bookmark-x:before { + content: "\e9aa"; +} +.icon-bookmark:before { + content: "\e9ab"; +} +.icon-box:before { + content: "\e9ac"; +} +.icon-brackets-check:before { + content: "\e9ad"; +} +.icon-brackets-ellipses:before { + content: "\e9ae"; +} +.icon-brackets-minus:before { + content: "\e9af"; +} +.icon-brackets-plus:before { + content: "\e9b0"; +} +.icon-brackets-slash:before { + content: "\e9b1"; +} +.icon-brackets-x:before { + content: "\e9b2"; +} +.icon-brackets:before { + content: "\e9b3"; +} +.icon-briefcase-01:before { + content: "\e9b4"; +} +.icon-briefcase-02:before { + content: "\e9b5"; +} +.icon-browser:before { + content: "\e9b6"; +} +.icon-brush-01:before { + content: "\e9b7"; +} +.icon-brush-02:before { + content: "\e9b8"; +} +.icon-brush-03:before { + content: "\e9b9"; +} +.icon-building-01:before { + content: "\e9ba"; +} +.icon-building-02:before { + content: "\e9bb"; +} +.icon-building-03:before { + content: "\e9bc"; +} +.icon-building-04:before { + content: "\e9bd"; +} +.icon-building-05:before { + content: "\e9be"; +} +.icon-building-06:before { + content: "\e9bf"; +} +.icon-building-07:before { + content: "\e9c0"; +} +.icon-building-08:before { + content: "\e9c1"; +} +.icon-bus:before { + content: "\e9c2"; +} +.icon-calculator:before { + content: "\e9c3"; +} +.icon-calendar-check-01:before { + content: "\e9c4"; +} +.icon-calendar-check-02:before { + content: "\e9c5"; +} +.icon-calendar-date:before { + content: "\e9c6"; +} +.icon-calendar-heart-01:before { + content: "\e9c7"; +} +.icon-calendar-heart-02:before { + content: "\e9c8"; +} +.icon-calendar-minus-01:before { + content: "\e9c9"; +} +.icon-calendar-minus-02:before { + content: "\e9ca"; +} +.icon-calendar-plus-01:before { + content: "\e9cb"; +} +.icon-calendar-plus-02:before { + content: "\e9cc"; +} +.icon-calendar:before { + content: "\e9cd"; +} +.icon-camera-01:before { + content: "\e9ce"; +} +.icon-camera-02:before { + content: "\e9cf"; +} +.icon-camera-03:before { + content: "\e9d0"; +} +.icon-camera-lens:before { + content: "\e9d1"; +} +.icon-camera-off:before { + content: "\e9d2"; +} +.icon-camera-plus:before { + content: "\e9d3"; +} +.icon-car-01:before { + content: "\e9d4"; +} +.icon-car-02:before { + content: "\e9d5"; +} +.icon-certificate-01:before { + content: "\e9d6"; +} +.icon-certificate-02:before { + content: "\e9d7"; +} +.icon-chart-breakout-circle:before { + content: "\e9d8"; +} +.icon-chart-breakout-square:before { + content: "\e9d9"; +} +.icon-check-circle-broken:before { + content: "\e9da"; +} +.icon-check-circle:before { + content: "\e9db"; +} +.icon-check-done-01:before { + content: "\e9dc"; +} +.icon-check-done-02:before { + content: "\e9dd"; +} +.icon-check-heart:before { + content: "\e9de"; +} +.icon-check-square-broken:before { + content: "\e9df"; +} +.icon-check-square:before { + content: "\e9e0"; +} +.icon-check-verified-01:before { + content: "\e9e1"; +} +.icon-check-verified-02:before { + content: "\e9e2"; +} +.icon-check-verified-03:before { + content: "\e9e3"; +} +.icon-check:before { + content: "\e9e4"; +} +.icon-chevron-down-double:before { + content: "\e9e5"; +} +.icon-chevron-down:before { + content: "\e9e6"; +} +.icon-chevron-left-double:before { + content: "\e9e7"; +} +.icon-chevron-left:before { + content: "\e9e8"; +} +.icon-chevron-right-double:before { + content: "\e9e9"; +} +.icon-chevron-right:before { + content: "\e9ea"; +} +.icon-chevron-selector-horizontal:before { + content: "\e9eb"; +} +.icon-chevron-selector-vertical:before { + content: "\e9ec"; +} +.icon-chevron-up-double:before { + content: "\e9ed"; +} +.icon-chevron-up:before { + content: "\e9ee"; +} +.icon-chrome-cast:before { + content: "\e9ef"; +} +.icon-circle-cut:before { + content: "\e9f0"; +} +.icon-circle:before { + content: "\e9f1"; +} +.icon-clapperboard:before { + content: "\e9f2"; +} +.icon-clipboard-attachment:before { + content: "\e9f3"; +} +.icon-clipboard-check:before { + content: "\e9f4"; +} +.icon-clipboard-download:before { + content: "\e9f5"; +} +.icon-clipboard-minus:before { + content: "\e9f6"; +} +.icon-clipboard-plus:before { + content: "\e9f7"; +} +.icon-clipboard-x:before { + content: "\e9f8"; +} +.icon-clipboard:before { + content: "\e9f9"; +} +.icon-clock-check:before { + content: "\e9fa"; +} +.icon-clock-fast-forward:before { + content: "\e9fb"; +} +.icon-clock-plus:before { + content: "\e9fc"; +} +.icon-clock-refresh:before { + content: "\e9fd"; +} +.icon-clock-rewind:before { + content: "\e9fe"; +} +.icon-clock-snooze:before { + content: "\e9ff"; +} +.icon-clock-stopwatch:before { + content: "\ea00"; +} +.icon-clock:before { + content: "\ea01"; +} +.icon-cloud-01:before { + content: "\ea02"; +} +.icon-cloud-02:before { + content: "\ea03"; +} +.icon-cloud-03:before { + content: "\ea04"; +} +.icon-cloud-blank-01:before { + content: "\ea05"; +} +.icon-cloud-blank-02:before { + content: "\ea06"; +} +.icon-cloud-lightning:before { + content: "\ea07"; +} +.icon-cloud-moon:before { + content: "\ea08"; +} +.icon-cloud-off:before { + content: "\ea09"; +} +.icon-cloud-raining-01:before { + content: "\ea0a"; +} +.icon-cloud-raining-02:before { + content: "\ea0b"; +} +.icon-cloud-raining-03:before { + content: "\ea0c"; +} +.icon-cloud-raining-04:before { + content: "\ea0d"; +} +.icon-cloud-raining-05:before { + content: "\ea0e"; +} +.icon-cloud-raining-06:before { + content: "\ea0f"; +} +.icon-cloud-snowing-01:before { + content: "\ea10"; +} +.icon-cloud-snowing-02:before { + content: "\ea11"; +} +.icon-cloud-sun-01:before { + content: "\ea12"; +} +.icon-cloud-sun-02:before { + content: "\ea13"; +} +.icon-cloud-sun-03:before { + content: "\ea14"; +} +.icon-code-01:before { + content: "\ea15"; +} +.icon-code-02:before { + content: "\ea16"; +} +.icon-code-browser:before { + content: "\ea17"; +} +.icon-code-circle-01:before { + content: "\ea18"; +} +.icon-code-circle-02:before { + content: "\ea19"; +} +.icon-code-circle-03:before { + content: "\ea1a"; +} +.icon-code-snippet-01:before { + content: "\ea1b"; +} +.icon-code-snippet-02:before { + content: "\ea1c"; +} +.icon-code-square-01:before { + content: "\ea1d"; +} +.icon-code-square-02:before { + content: "\ea1e"; +} +.icon-codepen:before { + content: "\ea1f"; +} +.icon-coins-01:before { + content: "\ea20"; +} +.icon-coins-02:before { + content: "\ea21"; +} +.icon-coins-03:before { + content: "\ea22"; +} +.icon-coins-04:before { + content: "\ea23"; +} +.icon-coins-hand:before { + content: "\ea24"; +} +.icon-coins-stacked-01:before { + content: "\ea25"; +} +.icon-coins-stacked-02:before { + content: "\ea26"; +} +.icon-coins-stacked-03:before { + content: "\ea27"; +} +.icon-coins-stacked-04:before { + content: "\ea28"; +} +.icon-coins-swap-01:before { + content: "\ea29"; +} +.icon-coins-swap-02:before { + content: "\ea2a"; +} +.icon-colors-2:before { + content: "\ea2b"; +} +.icon-colors:before { + content: "\ea2c"; +} +.icon-columns-01:before { + content: "\ea2d"; +} +.icon-columns-02:before { + content: "\ea2e"; +} +.icon-columns-03:before { + content: "\ea2f"; +} +.icon-command:before { + content: "\ea30"; +} +.icon-compass-01:before { + content: "\ea31"; +} +.icon-compass-02:before { + content: "\ea32"; +} +.icon-compass-03:before { + content: "\ea33"; +} +.icon-compass:before { + content: "\ea34"; +} +.icon-container:before { + content: "\ea35"; +} +.icon-contrast-01:before { + content: "\ea36"; +} +.icon-contrast-02:before { + content: "\ea37"; +} +.icon-contrast-03:before { + content: "\ea38"; +} +.icon-copy-01:before { + content: "\ea39"; +} +.icon-copy-02:before { + content: "\ea3a"; +} +.icon-copy-03:before { + content: "\ea3b"; +} +.icon-copy-04:before { + content: "\ea3c"; +} +.icon-copy-05:before { + content: "\ea3d"; +} +.icon-copy-06:before { + content: "\ea3e"; +} +.icon-copy-07:before { + content: "\ea3f"; +} +.icon-corner-down-left:before { + content: "\ea40"; +} +.icon-corner-down-right:before { + content: "\ea41"; +} +.icon-corner-left-down:before { + content: "\ea42"; +} +.icon-corner-left-up:before { + content: "\ea43"; +} +.icon-corner-right-down:before { + content: "\ea44"; +} +.icon-corner-right-up:before { + content: "\ea45"; +} +.icon-corner-up-left:before { + content: "\ea46"; +} +.icon-corner-up-right:before { + content: "\ea47"; +} +.icon-cpu-chip-01:before { + content: "\ea48"; +} +.icon-cpu-chip-02:before { + content: "\ea49"; +} +.icon-credit-card-01:before { + content: "\ea4a"; +} +.icon-credit-card-02:before { + content: "\ea4b"; +} +.icon-credit-card-check:before { + content: "\ea4c"; +} +.icon-credit-card-down:before { + content: "\ea4d"; +} +.icon-credit-card-download:before { + content: "\ea4e"; +} +.icon-credit-card-edit:before { + content: "\ea4f"; +} +.icon-credit-card-lock:before { + content: "\ea50"; +} +.icon-credit-card-minus:before { + content: "\ea51"; +} +.icon-credit-card-plus:before { + content: "\ea52"; +} +.icon-credit-card-refresh:before { + content: "\ea53"; +} +.icon-credit-card-search:before { + content: "\ea54"; +} +.icon-credit-card-shield:before { + content: "\ea55"; +} +.icon-credit-card-up:before { + content: "\ea56"; +} +.icon-credit-card-upload:before { + content: "\ea57"; +} +.icon-credit-card-x:before { + content: "\ea58"; +} +.icon-crop-01:before { + content: "\ea59"; +} +.icon-crop-02:before { + content: "\ea5a"; +} +.icon-cryptocurrency-01:before { + content: "\ea5b"; +} +.icon-cryptocurrency-02:before { + content: "\ea5c"; +} +.icon-cryptocurrency-03:before { + content: "\ea5d"; +} +.icon-cryptocurrency-04:before { + content: "\ea5e"; +} +.icon-cube-01:before { + content: "\ea5f"; +} +.icon-cube-02:before { + content: "\ea60"; +} +.icon-cube-03:before { + content: "\ea61"; +} +.icon-cube-04:before { + content: "\ea62"; +} +.icon-cube-outline:before { + content: "\ea63"; +} +.icon-currency-bitcoin-circle:before { + content: "\ea64"; +} +.icon-currency-bitcoin:before { + content: "\ea65"; +} +.icon-currency-dollar-circle:before { + content: "\ea66"; +} +.icon-currency-dollar:before { + content: "\ea67"; +} +.icon-currency-ethereum-circle:before { + content: "\ea68"; +} +.icon-currency-ethereum:before { + content: "\ea69"; +} +.icon-currency-euro-circle:before { + content: "\ea6a"; +} +.icon-currency-euro:before { + content: "\ea6b"; +} +.icon-currency-pound-circle:before { + content: "\ea6c"; +} +.icon-currency-pound:before { + content: "\ea6d"; +} +.icon-currency-ruble-circle:before { + content: "\ea6e"; +} +.icon-currency-ruble:before { + content: "\ea6f"; +} +.icon-currency-rupee-circle:before { + content: "\ea70"; +} +.icon-currency-rupee:before { + content: "\ea71"; +} +.icon-currency-yen-circle:before { + content: "\ea72"; +} +.icon-currency-yen:before { + content: "\ea73"; +} +.icon-cursor-01:before { + content: "\ea74"; +} +.icon-cursor-02:before { + content: "\ea75"; +} +.icon-cursor-03:before { + content: "\ea76"; +} +.icon-cursor-04:before { + content: "\ea77"; +} +.icon-cursor-box:before { + content: "\ea78"; +} +.icon-cursor-click-01:before { + content: "\ea79"; +} +.icon-cursor-click-02:before { + content: "\ea7a"; +} +.icon-data:before { + content: "\ea7b"; +} +.icon-database-01:before { + content: "\ea7c"; +} +.icon-database-02:before { + content: "\ea7d"; +} +.icon-database-03:before { + content: "\ea7e"; +} +.icon-dataflow-01:before { + content: "\ea7f"; +} +.icon-dataflow-02:before { + content: "\ea80"; +} +.icon-dataflow-03:before { + content: "\ea81"; +} +.icon-dataflow-04:before { + content: "\ea82"; +} +.icon-delete:before { + content: "\ea83"; +} +.icon-diamond-01:before { + content: "\ea84"; +} +.icon-diamond-02:before { + content: "\ea85"; +} +.icon-dice-1:before { + content: "\ea86"; +} +.icon-dice-2:before { + content: "\ea87"; +} +.icon-dice-3:before { + content: "\ea88"; +} +.icon-dice-4:before { + content: "\ea89"; +} +.icon-dice-5:before { + content: "\ea8a"; +} +.icon-dice-6:before { + content: "\ea8b"; +} +.icon-disc-01:before { + content: "\ea8c"; +} +.icon-disc-02:before { + content: "\ea8d"; +} +.icon-distribute-spacing-horizontal:before { + content: "\ea8e"; +} +.icon-distribute-spacing-vertical:before { + content: "\ea8f"; +} +.icon-divide-01:before { + content: "\ea90"; +} +.icon-divide-02:before { + content: "\ea91"; +} +.icon-divide-03:before { + content: "\ea92"; +} +.icon-divider:before { + content: "\ea93"; +} +.icon-dotpoints-01:before { + content: "\ea94"; +} +.icon-dotpoints-02:before { + content: "\ea95"; +} +.icon-dots-grid:before { + content: "\ea96"; +} +.icon-dots-horizontal:before { + content: "\ea97"; +} +.icon-dots-vertical:before { + content: "\ea98"; +} +.icon-download-01:before { + content: "\ea99"; +} +.icon-download-02:before { + content: "\ea9a"; +} +.icon-download-03:before { + content: "\ea9b"; +} +.icon-download-04:before { + content: "\ea9c"; +} +.icon-download-cloud-01:before { + content: "\ea9d"; +} +.icon-download-cloud-02:before { + content: "\ea9e"; +} +.icon-drop:before { + content: "\ea9f"; +} +.icon-droplets-01:before { + content: "\eaa0"; +} +.icon-droplets-02:before { + content: "\eaa1"; +} +.icon-droplets-03:before { + content: "\eaa2"; +} +.icon-dropper:before { + content: "\eaa3"; +} +.icon-edit-01:before { + content: "\eaa4"; +} +.icon-edit-02:before { + content: "\eaa5"; +} +.icon-edit-03:before { + content: "\eaa6"; +} +.icon-edit-04:before { + content: "\eaa7"; +} +.icon-edit-05:before { + content: "\eaa8"; +} +.icon-equal-not:before { + content: "\eaa9"; +} +.icon-equal:before { + content: "\eaaa"; +} +.icon-eraser:before { + content: "\eaab"; +} +.icon-expand-01:before { + content: "\eaac"; +} +.icon-expand-02:before { + content: "\eaad"; +} +.icon-expand-03:before { + content: "\eaae"; +} +.icon-expand-04:before { + content: "\eaaf"; +} +.icon-expand-05:before { + content: "\eab0"; +} +.icon-expand-06:before { + content: "\eab1"; +} +.icon-eye-off:before { + content: "\eab2"; +} +.icon-eye:before { + content: "\eab3"; +} +.icon-face-content:before { + content: "\eab4"; +} +.icon-face-frown:before { + content: "\eab5"; +} +.icon-face-happy:before { + content: "\eab6"; +} +.icon-face-id-square:before { + content: "\eab7"; +} +.icon-face-id:before { + content: "\eab8"; +} +.icon-face-neutral:before { + content: "\eab9"; +} +.icon-face-sad:before { + content: "\eaba"; +} +.icon-face-smile:before { + content: "\eabb"; +} +.icon-face-wink:before { + content: "\eabc"; +} +.icon-fast-backward:before { + content: "\eabd"; +} +.icon-fast-forward:before { + content: "\eabe"; +} +.icon-feather:before { + content: "\eabf"; +} +.icon-figma:before { + content: "\eac0"; +} +.icon-file-01:before { + content: "\eac1"; +} +.icon-file-02:before { + content: "\eac2"; +} +.icon-file-03:before { + content: "\eac3"; +} +.icon-file-04:before { + content: "\eac4"; +} +.icon-file-05:before { + content: "\eac5"; +} +.icon-file-06:before { + content: "\eac6"; +} +.icon-file-07:before { + content: "\eac7"; +} +.icon-file-attachment-01:before { + content: "\eac8"; +} +.icon-file-attachment-02:before { + content: "\eac9"; +} +.icon-file-attachment-03:before { + content: "\eaca"; +} +.icon-file-attachment-04:before { + content: "\eacb"; +} +.icon-file-attachment-05:before { + content: "\eacc"; +} +.icon-file-check-01:before { + content: "\eacd"; +} +.icon-file-check-02:before { + content: "\eace"; +} +.icon-file-check-03:before { + content: "\eacf"; +} +.icon-file-code-01:before { + content: "\ead0"; +} +.icon-file-code-02:before { + content: "\ead1"; +} +.icon-file-download-01:before { + content: "\ead2"; +} +.icon-file-download-02:before { + content: "\ead3"; +} +.icon-file-download-03:before { + content: "\ead4"; +} +.icon-file-heart-01:before { + content: "\ead5"; +} +.icon-file-heart-02:before { + content: "\ead6"; +} +.icon-file-heart-03:before { + content: "\ead7"; +} +.icon-file-lock-01:before { + content: "\ead8"; +} +.icon-file-lock-02:before { + content: "\ead9"; +} +.icon-file-lock-03:before { + content: "\eada"; +} +.icon-file-minus-01:before { + content: "\eadb"; +} +.icon-file-minus-02:before { + content: "\eadc"; +} +.icon-file-minus-03:before { + content: "\eadd"; +} +.icon-file-plus-01:before { + content: "\eade"; +} +.icon-file-plus-02:before { + content: "\eadf"; +} +.icon-file-plus-03:before { + content: "\eae0"; +} +.icon-file-question-01:before { + content: "\eae1"; +} +.icon-file-question-02:before { + content: "\eae2"; +} +.icon-file-question-03:before { + content: "\eae3"; +} +.icon-file-search-01:before { + content: "\eae4"; +} +.icon-file-search-02:before { + content: "\eae5"; +} +.icon-file-search-03:before { + content: "\eae6"; +} +.icon-file-shield-01:before { + content: "\eae7"; +} +.icon-file-shield-02:before { + content: "\eae8"; +} +.icon-file-shield-03:before { + content: "\eae9"; +} +.icon-file-x-01:before { + content: "\eaea"; +} +.icon-file-x-02:before { + content: "\eaeb"; +} +.icon-file-x-03:before { + content: "\eaec"; +} +.icon-film-01:before { + content: "\eaed"; +} +.icon-film-02:before { + content: "\eaee"; +} +.icon-film-03:before { + content: "\eaef"; +} +.icon-filter-funnel-01:before { + content: "\eaf0"; +} +.icon-filter-funnel-02:before { + content: "\eaf1"; +} +.icon-filter-lines:before { + content: "\eaf2"; +} +.icon-fingerprint-01:before { + content: "\eaf3"; +} +.icon-fingerprint-02:before { + content: "\eaf4"; +} +.icon-fingerprint-03:before { + content: "\eaf5"; +} +.icon-fingerprint-04:before { + content: "\eaf6"; +} +.icon-flag-01:before { + content: "\eaf7"; +} +.icon-flag-02:before { + content: "\eaf8"; +} +.icon-flag-03:before { + content: "\eaf9"; +} +.icon-flag-04:before { + content: "\eafa"; +} +.icon-flag-05:before { + content: "\eafb"; +} +.icon-flag-06:before { + content: "\eafc"; +} +.icon-flash-off:before { + content: "\eafd"; +} +.icon-flash:before { + content: "\eafe"; +} +.icon-flex-align-bottom:before { + content: "\eaff"; +} +.icon-flex-align-left:before { + content: "\eb00"; +} +.icon-flex-align-right:before { + content: "\eb01"; +} +.icon-flex-align-top:before { + content: "\eb02"; +} +.icon-flip-backward:before { + content: "\eb03"; +} +.icon-flip-forward:before { + content: "\eb04"; +} +.icon-folder-check:before { + content: "\eb05"; +} +.icon-folder-closed:before { + content: "\eb06"; +} +.icon-folder-code:before { + content: "\eb07"; +} +.icon-folder-download:before { + content: "\eb08"; +} +.icon-folder-lock:before { + content: "\eb09"; +} +.icon-folder-minus:before { + content: "\eb0a"; +} +.icon-folder-plus:before { + content: "\eb0b"; +} +.icon-folder-question:before { + content: "\eb0c"; +} +.icon-folder-search:before { + content: "\eb0d"; +} +.icon-folder-shield:before { + content: "\eb0e"; +} +.icon-folder-x:before { + content: "\eb0f"; +} +.icon-folder:before { + content: "\eb10"; +} +.icon-framer:before { + content: "\eb11"; +} +.icon-gaming-pad-01:before { + content: "\eb12"; +} +.icon-gaming-pad-02:before { + content: "\eb13"; +} +.icon-gift-01:before { + content: "\eb14"; +} +.icon-gift-02:before { + content: "\eb15"; +} +.icon-git-branch-01:before { + content: "\eb16"; +} +.icon-git-branch-02:before { + content: "\eb17"; +} +.icon-git-commit:before { + content: "\eb18"; +} +.icon-git-merge:before { + content: "\eb19"; +} +.icon-git-pull-request:before { + content: "\eb1a"; +} +.icon-glasses-01:before { + content: "\eb1b"; +} +.icon-glasses-02:before { + content: "\eb1c"; +} +.icon-globe-01:before { + content: "\eb1d"; +} +.icon-globe-02:before { + content: "\eb1e"; +} +.icon-globe-03:before { + content: "\eb1f"; +} +.icon-globe-04:before { + content: "\eb20"; +} +.icon-globe-05:before { + content: "\eb21"; +} +.icon-globe-06:before { + content: "\eb22"; +} +.icon-globe-slated-01:before { + content: "\eb23"; +} +.icon-globe-slated-02:before { + content: "\eb24"; +} +.icon-google-chrome:before { + content: "\eb25"; +} +.icon-graduation-hat-01:before { + content: "\eb26"; +} +.icon-graduation-hat-02:before { + content: "\eb27"; +} +.icon-grid-01:before { + content: "\eb28"; +} +.icon-grid-02:before { + content: "\eb29"; +} +.icon-grid-03:before { + content: "\eb2a"; +} +.icon-grid-dots-blank:before { + content: "\eb2b"; +} +.icon-grid-dots-bottom:before { + content: "\eb2c"; +} +.icon-grid-dots-horizontal-center:before { + content: "\eb2d"; +} +.icon-grid-dots-left:before { + content: "\eb2e"; +} +.icon-grid-dots-outer:before { + content: "\eb2f"; +} +.icon-grid-dots-right:before { + content: "\eb30"; +} +.icon-grid-dots-top:before { + content: "\eb31"; +} +.icon-grid-dots-vertical-center:before { + content: "\eb32"; +} +.icon-hand:before { + content: "\eb33"; +} +.icon-hard-drive:before { + content: "\eb34"; +} +.icon-hash-01:before { + content: "\eb35"; +} +.icon-hash-02:before { + content: "\eb36"; +} +.icon-heading-01:before { + content: "\eb37"; +} +.icon-heading-02:before { + content: "\eb38"; +} +.icon-heading-square:before { + content: "\eb39"; +} +.icon-headphones-01:before { + content: "\eb3a"; +} +.icon-headphones-02:before { + content: "\eb3b"; +} +.icon-heart-circle:before { + content: "\eb3c"; +} +.icon-heart-hand:before { + content: "\eb3d"; +} +.icon-heart-hexagon:before { + content: "\eb3e"; +} +.icon-heart-octagon:before { + content: "\eb3f"; +} +.icon-heart-rounded:before { + content: "\eb40"; +} +.icon-heart-square:before { + content: "\eb41"; +} +.icon-heart:before { + content: "\eb42"; +} +.icon-hearts:before { + content: "\eb43"; +} +.icon-help-circle:before { + content: "\eb44"; +} +.icon-help-hexagon:before { + content: "\eb45"; +} +.icon-help-octagon:before { + content: "\eb46"; +} +.icon-help-square:before { + content: "\eb47"; +} +.icon-hexagon-01:before { + content: "\eb48"; +} +.icon-hexagon-02:before { + content: "\eb49"; +} +.icon-home-01:before { + content: "\eb4a"; +} +.icon-home-02:before { + content: "\eb4b"; +} +.icon-home-03:before { + content: "\eb4c"; +} +.icon-home-04:before { + content: "\eb4d"; +} +.icon-home-05:before { + content: "\eb4e"; +} +.icon-home-line:before { + content: "\eb4f"; +} +.icon-home-smile:before { + content: "\eb50"; +} +.icon-horizontal-bar-chart-01:before { + content: "\eb51"; +} +.icon-horizontal-bar-chart-02:before { + content: "\eb52"; +} +.icon-horizontal-bar-chart-03:before { + content: "\eb53"; +} +.icon-hourglass-01:before { + content: "\eb54"; +} +.icon-hourglass-02:before { + content: "\eb55"; +} +.icon-hourglass-03:before { + content: "\eb56"; +} +.icon-hurricane-01:before { + content: "\eb57"; +} +.icon-hurricane-02:before { + content: "\eb58"; +} +.icon-hurricane-03:before { + content: "\eb59"; +} +.icon-image-01:before { + content: "\eb5a"; +} +.icon-image-02:before { + content: "\eb5b"; +} +.icon-image-03:before { + content: "\eb5c"; +} +.icon-image-04:before { + content: "\eb5d"; +} +.icon-image-05:before { + content: "\eb5e"; +} +.icon-image-check:before { + content: "\eb5f"; +} +.icon-image-down:before { + content: "\eb60"; +} +.icon-image-indent-left:before { + content: "\eb61"; +} +.icon-image-indent-right:before { + content: "\eb62"; +} +.icon-image-left:before { + content: "\eb63"; +} +.icon-image-plus:before { + content: "\eb64"; +} +.icon-image-right:before { + content: "\eb65"; +} +.icon-image-up:before { + content: "\eb66"; +} +.icon-image-user-check:before { + content: "\eb67"; +} +.icon-image-user-down:before { + content: "\eb68"; +} +.icon-image-user-left:before { + content: "\eb69"; +} +.icon-image-user-plus:before { + content: "\eb6a"; +} +.icon-image-user-right:before { + content: "\eb6b"; +} +.icon-image-user-up:before { + content: "\eb6c"; +} +.icon-image-user-x:before { + content: "\eb6d"; +} +.icon-image-user:before { + content: "\eb6e"; +} +.icon-image-x:before { + content: "\eb6f"; +} +.icon-inbox-01:before { + content: "\eb70"; +} +.icon-inbox-02:before { + content: "\eb71"; +} +.icon-infinity:before { + content: "\eb72"; +} +.icon-info-circle:before { + content: "\eb73"; +} +.icon-info-hexagon:before { + content: "\eb74"; +} +.icon-info-octagon:before { + content: "\eb75"; +} +.icon-info-square:before { + content: "\eb76"; +} +.icon-intersect-circle:before { + content: "\eb77"; +} +.icon-intersect-square:before { + content: "\eb78"; +} +.icon-italic-01:before { + content: "\eb79"; +} +.icon-italic-02:before { + content: "\eb7a"; +} +.icon-italic-square:before { + content: "\eb7b"; +} +.icon-key-01:before { + content: "\eb7c"; +} +.icon-key-02:before { + content: "\eb7d"; +} +.icon-keyboard-01:before { + content: "\eb7e"; +} +.icon-keyboard-02:before { + content: "\eb7f"; +} +.icon-laptop-01:before { + content: "\eb80"; +} +.icon-laptop-02:before { + content: "\eb81"; +} +.icon-layer-single:before { + content: "\eb82"; +} +.icon-layers-three-01:before { + content: "\eb83"; +} +.icon-layers-three-02:before { + content: "\eb84"; +} +.icon-layers-two-01:before { + content: "\eb85"; +} +.icon-layers-two-02:before { + content: "\eb86"; +} +.icon-layout-alt-01:before { + content: "\eb87"; +} +.icon-layout-alt-02:before { + content: "\eb88"; +} +.icon-layout-alt-03:before { + content: "\eb89"; +} +.icon-layout-alt-04:before { + content: "\eb8a"; +} +.icon-layout-bottom:before { + content: "\eb8b"; +} +.icon-layout-grid-01:before { + content: "\eb8c"; +} +.icon-layout-grid-02:before { + content: "\eb8d"; +} +.icon-layout-left:before { + content: "\eb8e"; +} +.icon-layout-right:before { + content: "\eb8f"; +} +.icon-layout-top:before { + content: "\eb90"; +} +.icon-left-indent-01:before { + content: "\eb91"; +} +.icon-left-indent-02:before { + content: "\eb92"; +} +.icon-letter-spacing-01:before { + content: "\eb93"; +} +.icon-letter-spacing-02:before { + content: "\eb94"; +} +.icon-life-buoy-01:before { + content: "\eb95"; +} +.icon-life-buoy-02:before { + content: "\eb96"; +} +.icon-lightbulb-01:before { + content: "\eb97"; +} +.icon-lightbulb-02:before { + content: "\eb98"; +} +.icon-lightbulb-03:before { + content: "\eb99"; +} +.icon-lightbulb-04:before { + content: "\eb9a"; +} +.icon-lightbulb-05:before { + content: "\eb9b"; +} +.icon-lightning-01:before { + content: "\eb9c"; +} +.icon-lightning-02:before { + content: "\eb9d"; +} +.icon-line-chart-down-01:before { + content: "\eb9e"; +} +.icon-line-chart-down-02:before { + content: "\eb9f"; +} +.icon-line-chart-down-03:before { + content: "\eba0"; +} +.icon-line-chart-down-04:before { + content: "\eba1"; +} +.icon-line-chart-down-05:before { + content: "\eba2"; +} +.icon-line-chart-up-01:before { + content: "\eba3"; +} +.icon-line-chart-up-02:before { + content: "\eba4"; +} +.icon-line-chart-up-03:before { + content: "\eba5"; +} +.icon-line-chart-up-04:before { + content: "\eba6"; +} +.icon-line-chart-up-05:before { + content: "\eba7"; +} +.icon-line-height:before { + content: "\eba8"; +} +.icon-link-01:before { + content: "\eba9"; +} +.icon-link-02:before { + content: "\ebaa"; +} +.icon-link-03:before { + content: "\ebab"; +} +.icon-link-04:before { + content: "\ebac"; +} +.icon-link-05:before { + content: "\ebad"; +} +.icon-link-broken-01:before { + content: "\ebae"; +} +.icon-link-broken-02:before { + content: "\ebaf"; +} +.icon-link-external-01:before { + content: "\ebb0"; +} +.icon-link-external-02:before { + content: "\ebb1"; +} +.icon-list:before { + content: "\ebb2"; +} +.icon-loading-01:before { + content: "\ebb3"; +} +.icon-loading-02:before { + content: "\ebb4"; +} +.icon-loading-03:before { + content: "\ebb5"; +} +.icon-lock-01:before { + content: "\ebb6"; +} +.icon-lock-02:before { + content: "\ebb7"; +} +.icon-lock-03:before { + content: "\ebb8"; +} +.icon-lock-04:before { + content: "\ebb9"; +} +.icon-lock-keyhole-circle:before { + content: "\ebba"; +} +.icon-lock-keyhole-square:before { + content: "\ebbb"; +} +.icon-lock-unlocked-01:before { + content: "\ebbc"; +} +.icon-lock-unlocked-02:before { + content: "\ebbd"; +} +.icon-lock-unlocked-03:before { + content: "\ebbe"; +} +.icon-lock-unlocked-04:before { + content: "\ebbf"; +} +.icon-log-in-01:before { + content: "\ebc0"; +} +.icon-log-in-02:before { + content: "\ebc1"; +} +.icon-log-in-03:before { + content: "\ebc2"; +} +.icon-log-in-04:before { + content: "\ebc3"; +} +.icon-log-out-01:before { + content: "\ebc4"; +} +.icon-log-out-02:before { + content: "\ebc5"; +} +.icon-log-out-03:before { + content: "\ebc6"; +} +.icon-log-out-04:before { + content: "\ebc7"; +} +.icon-luggage-01:before { + content: "\ebc8"; +} +.icon-luggage-02:before { + content: "\ebc9"; +} +.icon-luggage-03:before { + content: "\ebca"; +} +.icon-magic-wand-01:before { + content: "\ebcb"; +} +.icon-magic-wand-02:before { + content: "\ebcc"; +} +.icon-mail-01:before { + content: "\ebcd"; +} +.icon-mail-02:before { + content: "\ebce"; +} +.icon-mail-03:before { + content: "\ebcf"; +} +.icon-mail-04:before { + content: "\ebd0"; +} +.icon-mail-05:before { + content: "\ebd1"; +} +.icon-map-01:before { + content: "\ebd2"; +} +.icon-map-02:before { + content: "\ebd3"; +} +.icon-mark:before { + content: "\ebd4"; +} +.icon-marker-pin-01:before { + content: "\ebd5"; +} +.icon-marker-pin-02:before { + content: "\ebd6"; +} +.icon-marker-pin-03:before { + content: "\ebd7"; +} +.icon-marker-pin-04:before { + content: "\ebd8"; +} +.icon-marker-pin-05:before { + content: "\ebd9"; +} +.icon-marker-pin-06:before { + content: "\ebda"; +} +.icon-maximize-01:before { + content: "\ebdb"; +} +.icon-maximize-02:before { + content: "\ebdc"; +} +.icon-medical-circle:before { + content: "\ebdd"; +} +.icon-medical-cross:before { + content: "\ebde"; +} +.icon-medical-square:before { + content: "\ebdf"; +} +.icon-menu-01:before { + content: "\ebe0"; +} +.icon-menu-02:before { + content: "\ebe1"; +} +.icon-menu-03:before { + content: "\ebe2"; +} +.icon-menu-04:before { + content: "\ebe3"; +} +.icon-menu-05:before { + content: "\ebe4"; +} +.icon-message-alert-circle:before { + content: "\ebe5"; +} +.icon-message-alert-square:before { + content: "\ebe6"; +} +.icon-message-chat-circle:before { + content: "\ebe7"; +} +.icon-message-chat-square:before { + content: "\ebe8"; +} +.icon-message-check-circle:before { + content: "\ebe9"; +} +.icon-message-check-square:before { + content: "\ebea"; +} +.icon-message-circle-01:before { + content: "\ebeb"; +} +.icon-message-circle-02:before { + content: "\ebec"; +} +.icon-message-dots-circle:before { + content: "\ebed"; +} +.icon-message-dots-square:before { + content: "\ebee"; +} +.icon-message-heart-circle:before { + content: "\ebef"; +} +.icon-message-heart-square:before { + content: "\ebf0"; +} +.icon-message-notification-circle:before { + content: "\ebf1"; +} +.icon-message-notification-square:before { + content: "\ebf2"; +} +.icon-message-plus-circle:before { + content: "\ebf3"; +} +.icon-message-plus-square:before { + content: "\ebf4"; +} +.icon-message-question-circle:before { + content: "\ebf5"; +} +.icon-message-question-square:before { + content: "\ebf6"; +} +.icon-message-smile-circle:before { + content: "\ebf7"; +} +.icon-message-smile-square:before { + content: "\ebf8"; +} +.icon-message-square-01:before { + content: "\ebf9"; +} +.icon-message-square-02:before { + content: "\ebfa"; +} +.icon-message-text-circle-01:before { + content: "\ebfb"; +} +.icon-message-text-circle-02:before { + content: "\ebfc"; +} +.icon-message-text-square-01:before { + content: "\ebfd"; +} +.icon-message-text-square-02:before { + content: "\ebfe"; +} +.icon-message-x-circle:before { + content: "\ebff"; +} +.icon-message-x-square:before { + content: "\ec00"; +} +.icon-microphone-01:before { + content: "\ec01"; +} +.icon-microphone-02:before { + content: "\ec02"; +} +.icon-microphone-off-01:before { + content: "\ec03"; +} +.icon-microphone-off-02:before { + content: "\ec04"; +} +.icon-microscope:before { + content: "\ec05"; +} +.icon-minimize-01:before { + content: "\ec06"; +} +.icon-minimize-02:before { + content: "\ec07"; +} +.icon-minus-circle:before { + content: "\ec08"; +} +.icon-minus-square:before { + content: "\ec09"; +} +.icon-minus:before { + content: "\ec0a"; +} +.icon-modem-01:before { + content: "\ec0b"; +} +.icon-modem-02:before { + content: "\ec0c"; +} +.icon-monitor-01:before { + content: "\ec0d"; +} +.icon-monitor-02:before { + content: "\ec0e"; +} +.icon-monitor-03:before { + content: "\ec0f"; +} +.icon-monitor-04:before { + content: "\ec10"; +} +.icon-monitor-05:before { + content: "\ec11"; +} +.icon-moon-01:before { + content: "\ec12"; +} +.icon-moon-02:before { + content: "\ec13"; +} +.icon-moon-eclipse:before { + content: "\ec14"; +} +.icon-moon-star:before { + content: "\ec15"; +} +.icon-mouse:before { + content: "\ec16"; +} +.icon-move:before { + content: "\ec17"; +} +.icon-music-note-01:before { + content: "\ec18"; +} +.icon-music-note-02:before { + content: "\ec19"; +} +.icon-music-note-plus:before { + content: "\ec1a"; +} +.icon-navigation-pointer-01:before { + content: "\ec1b"; +} +.icon-navigation-pointer-02:before { + content: "\ec1c"; +} +.icon-navigation-pointer-off-01:before { + content: "\ec1d"; +} +.icon-navigation-pointer-off-02:before { + content: "\ec1e"; +} +.icon-notification-box:before { + content: "\ec1f"; +} +.icon-notification-message:before { + content: "\ec20"; +} +.icon-notification-text:before { + content: "\ec21"; +} +.icon-octagon:before { + content: "\ec22"; +} +.icon-package-check:before { + content: "\ec23"; +} +.icon-package-minus:before { + content: "\ec24"; +} +.icon-package-plus:before { + content: "\ec25"; +} +.icon-package-search:before { + content: "\ec26"; +} +.icon-package-x:before { + content: "\ec27"; +} +.icon-package:before { + content: "\ec28"; +} +.icon-paint-pour:before { + content: "\ec29"; +} +.icon-paint:before { + content: "\ec2a"; +} +.icon-palette:before { + content: "\ec2b"; +} +.icon-paperclip:before { + content: "\ec2c"; +} +.icon-paragraph-spacing:before { + content: "\ec2d"; +} +.icon-paragraph-wrap:before { + content: "\ec2e"; +} +.icon-passcode-lock:before { + content: "\ec2f"; +} +.icon-passcode:before { + content: "\ec30"; +} +.icon-passport:before { + content: "\ec31"; +} +.icon-pause-circle:before { + content: "\ec32"; +} +.icon-pause-square:before { + content: "\ec33"; +} +.icon-pen-tool-01:before { + content: "\ec34"; +} +.icon-pen-tool-02:before { + content: "\ec35"; +} +.icon-pen-tool-minus:before { + content: "\ec36"; +} +.icon-pen-tool-plus:before { + content: "\ec37"; +} +.icon-pencil-01:before { + content: "\ec38"; +} +.icon-pencil-02:before { + content: "\ec39"; +} +.icon-pencil-line:before { + content: "\ec3a"; +} +.icon-pentagon:before { + content: "\ec3b"; +} +.icon-percent-01:before { + content: "\ec3c"; +} +.icon-percent-02:before { + content: "\ec3d"; +} +.icon-percent-03:before { + content: "\ec3e"; +} +.icon-perspective-01:before { + content: "\ec3f"; +} +.icon-perspective-02:before { + content: "\ec40"; +} +.icon-phone-01:before { + content: "\ec41"; +} +.icon-phone-02:before { + content: "\ec42"; +} +.icon-phone-call-01:before { + content: "\ec43"; +} +.icon-phone-call-02:before { + content: "\ec44"; +} +.icon-phone-hang-up:before { + content: "\ec45"; +} +.icon-phone-incoming-01:before { + content: "\ec46"; +} +.icon-phone-incoming-02:before { + content: "\ec47"; +} +.icon-phone-outgoing-01:before { + content: "\ec48"; +} +.icon-phone-outgoing-02:before { + content: "\ec49"; +} +.icon-phone-pause:before { + content: "\ec4a"; +} +.icon-phone-plus:before { + content: "\ec4b"; +} +.icon-phone-x:before { + content: "\ec4c"; +} +.icon-phone:before { + content: "\ec4d"; +} +.icon-pie-chart-01:before { + content: "\ec4e"; +} +.icon-pie-chart-02:before { + content: "\ec4f"; +} +.icon-pie-chart-03:before { + content: "\ec50"; +} +.icon-pie-chart-04:before { + content: "\ec51"; +} +.icon-piggy-bank-01:before { + content: "\ec52"; +} +.icon-piggy-bank-02:before { + content: "\ec53"; +} +.icon-pilcrow-01:before { + content: "\ec54"; +} +.icon-pilcrow-02:before { + content: "\ec55"; +} +.icon-pilcrow-square:before { + content: "\ec56"; +} +.icon-pin-01:before { + content: "\ec57"; +} +.icon-pin-02:before { + content: "\ec58"; +} +.icon-placeholder:before { + content: "\ec59"; +} +.icon-plane:before { + content: "\ec5a"; +} +.icon-play-circle:before { + content: "\ec5b"; +} +.icon-play-square:before { + content: "\ec5c"; +} +.icon-play:before { + content: "\ec5d"; +} +.icon-plus-circle:before { + content: "\ec5e"; +} +.icon-plus-square:before { + content: "\ec5f"; +} +.icon-plus:before { + content: "\ec60"; +} +.icon-podcast:before { + content: "\ec61"; +} +.icon-power-01:before { + content: "\ec62"; +} +.icon-power-02:before { + content: "\ec63"; +} +.icon-power-03:before { + content: "\ec64"; +} +.icon-presentation-chart-01:before { + content: "\ec65"; +} +.icon-presentation-chart-02:before { + content: "\ec66"; +} +.icon-presentation-chart-03:before { + content: "\ec67"; +} +.icon-printer:before { + content: "\ec68"; +} +.icon-puzzle-piece-01:before { + content: "\ec69"; +} +.icon-puzzle-piece-02:before { + content: "\ec6a"; +} +.icon-qr-code-01:before { + content: "\ec6b"; +} +.icon-qr-code-02:before { + content: "\ec6c"; +} +.icon-receipt-check:before { + content: "\ec6d"; +} +.icon-receipt:before { + content: "\ec6e"; +} +.icon-recording-01:before { + content: "\ec6f"; +} +.icon-recording-02:before { + content: "\ec70"; +} +.icon-recording-03:before { + content: "\ec71"; +} +.icon-reflect-01:before { + content: "\ec72"; +} +.icon-reflect-02:before { + content: "\ec73"; +} +.icon-refresh-ccw-01:before { + content: "\ec74"; +} +.icon-refresh-ccw-02:before { + content: "\ec75"; +} +.icon-refresh-ccw-03:before { + content: "\ec76"; +} +.icon-refresh-ccw-04:before { + content: "\ec77"; +} +.icon-refresh-ccw-05:before { + content: "\ec78"; +} +.icon-refresh-cw-01:before { + content: "\ec79"; +} +.icon-refresh-cw-02:before { + content: "\ec7a"; +} +.icon-refresh-cw-03:before { + content: "\ec7b"; +} +.icon-refresh-cw-04:before { + content: "\ec7c"; +} +.icon-refresh-cw-05:before { + content: "\ec7d"; +} +.icon-repeat-01:before { + content: "\ec7e"; +} +.icon-repeat-02:before { + content: "\ec7f"; +} +.icon-repeat-03:before { + content: "\ec80"; +} +.icon-repeat-04:before { + content: "\ec81"; +} +.icon-reverse-left:before { + content: "\ec82"; +} +.icon-reverse-right:before { + content: "\ec83"; +} +.icon-right-indent-01:before { + content: "\ec84"; +} +.icon-right-indent-02:before { + content: "\ec85"; +} +.icon-rocket-01:before { + content: "\ec86"; +} +.icon-rocket-02:before { + content: "\ec87"; +} +.icon-roller-brush:before { + content: "\ec88"; +} +.icon-route:before { + content: "\ec89"; +} +.icon-rows-01:before { + content: "\ec8a"; +} +.icon-rows-02:before { + content: "\ec8b"; +} +.icon-rows-03:before { + content: "\ec8c"; +} +.icon-rss-01:before { + content: "\ec8d"; +} +.icon-rss-02:before { + content: "\ec8e"; +} +.icon-ruler:before { + content: "\ec8f"; +} +.icon-safe:before { + content: "\ec90"; +} +.icon-sale-01:before { + content: "\ec91"; +} +.icon-sale-02:before { + content: "\ec92"; +} +.icon-sale-03:before { + content: "\ec93"; +} +.icon-sale-04:before { + content: "\ec94"; +} +.icon-save-01:before { + content: "\ec95"; +} +.icon-save-02:before { + content: "\ec96"; +} +.icon-save-03:before { + content: "\ec97"; +} +.icon-scale-01:before { + content: "\ec98"; +} +.icon-scale-02:before { + content: "\ec99"; +} +.icon-scale-03:before { + content: "\ec9a"; +} +.icon-scales-01:before { + content: "\ec9b"; +} +.icon-scales-02:before { + content: "\ec9c"; +} +.icon-scan:before { + content: "\ec9d"; +} +.icon-scissors-01:before { + content: "\ec9e"; +} +.icon-scissors-02:before { + content: "\ec9f"; +} +.icon-scissors-cut-01:before { + content: "\eca0"; +} +.icon-scissors-cut-02:before { + content: "\eca1"; +} +.icon-search-lg:before { + content: "\eca2"; +} +.icon-search-md:before { + content: "\eca3"; +} +.icon-search-refraction:before { + content: "\eca4"; +} +.icon-search-sm:before { + content: "\eca5"; +} +.icon-send-01:before { + content: "\eca6"; +} +.icon-send-02:before { + content: "\eca7"; +} +.icon-send-03:before { + content: "\eca8"; +} +.icon-server-01:before { + content: "\eca9"; +} +.icon-server-02:before { + content: "\ecaa"; +} +.icon-server-03:before { + content: "\ecab"; +} +.icon-server-04:before { + content: "\ecac"; +} +.icon-server-05:before { + content: "\ecad"; +} +.icon-server-06:before { + content: "\ecae"; +} +.icon-settings-01:before { + content: "\ecaf"; +} +.icon-settings-02:before { + content: "\ecb0"; +} +.icon-settings-03:before { + content: "\ecb1"; +} +.icon-settings-04:before { + content: "\ecb2"; +} +.icon-share-01:before { + content: "\ecb3"; +} +.icon-share-02:before { + content: "\ecb4"; +} +.icon-share-03:before { + content: "\ecb5"; +} +.icon-share-04:before { + content: "\ecb6"; +} +.icon-share-05:before { + content: "\ecb7"; +} +.icon-share-06:before { + content: "\ecb8"; +} +.icon-share-07:before { + content: "\ecb9"; +} +.icon-shield-01:before { + content: "\ecba"; +} +.icon-shield-02:before { + content: "\ecbb"; +} +.icon-shield-03:before { + content: "\ecbc"; +} +.icon-shield-dollar:before { + content: "\ecbd"; +} +.icon-shield-off:before { + content: "\ecbe"; +} +.icon-shield-plus:before { + content: "\ecbf"; +} +.icon-shield-tick:before { + content: "\ecc0"; +} +.icon-shield-zap:before { + content: "\ecc1"; +} +.icon-shopping-bag-01:before { + content: "\ecc2"; +} +.icon-shopping-bag-02:before { + content: "\ecc3"; +} +.icon-shopping-bag-03:before { + content: "\ecc4"; +} +.icon-shopping-cart-01:before { + content: "\ecc5"; +} +.icon-shopping-cart-02:before { + content: "\ecc6"; +} +.icon-shopping-cart-03:before { + content: "\ecc7"; +} +.icon-shuffle-01:before { + content: "\ecc8"; +} +.icon-shuffle-02:before { + content: "\ecc9"; +} +.icon-signal-01:before { + content: "\ecca"; +} +.icon-signal-02:before { + content: "\eccb"; +} +.icon-signal-03:before { + content: "\eccc"; +} +.icon-simcard:before { + content: "\eccd"; +} +.icon-skew:before { + content: "\ecce"; +} +.icon-skip-back:before { + content: "\eccf"; +} +.icon-skip-forward:before { + content: "\ecd0"; +} +.icon-slash-circle-01:before { + content: "\ecd1"; +} +.icon-slash-circle-02:before { + content: "\ecd2"; +} +.icon-slash-divider:before { + content: "\ecd3"; +} +.icon-slash-octagon:before { + content: "\ecd4"; +} +.icon-sliders-01:before { + content: "\ecd5"; +} +.icon-sliders-02:before { + content: "\ecd6"; +} +.icon-sliders-03:before { + content: "\ecd7"; +} +.icon-sliders-04:before { + content: "\ecd8"; +} +.icon-snowflake-01:before { + content: "\ecd9"; +} +.icon-snowflake-02:before { + content: "\ecda"; +} +.icon-spacing-height-01:before { + content: "\ecdb"; +} +.icon-spacing-height-02:before { + content: "\ecdc"; +} +.icon-spacing-width-01:before { + content: "\ecdd"; +} +.icon-spacing-width-02:before { + content: "\ecde"; +} +.icon-speaker-01:before { + content: "\ecdf"; +} +.icon-speaker-02:before { + content: "\ece0"; +} +.icon-speaker-03:before { + content: "\ece1"; +} +.icon-speedometer-01:before { + content: "\ece2"; +} +.icon-speedometer-02:before { + content: "\ece3"; +} +.icon-speedometer-03:before { + content: "\ece4"; +} +.icon-speedometer-04:before { + content: "\ece5"; +} +.icon-square:before { + content: "\ece6"; +} +.icon-stand:before { + content: "\ece7"; +} +.icon-star-01:before { + content: "\ece8"; +} +.icon-star-02:before { + content: "\ece9"; +} +.icon-star-03:before { + content: "\ecea"; +} +.icon-star-04:before { + content: "\eceb"; +} +.icon-star-05:before { + content: "\ecec"; +} +.icon-star-06:before { + content: "\eced"; +} +.icon-star-07:before { + content: "\ecee"; +} +.icon-stars-01:before { + content: "\ecef"; +} +.icon-stars-02:before { + content: "\ecf0"; +} +.icon-stars-03:before { + content: "\ecf1"; +} +.icon-sticker-circle:before { + content: "\ecf2"; +} +.icon-sticker-square:before { + content: "\ecf3"; +} +.icon-stop-circle:before { + content: "\ecf4"; +} +.icon-stop-square:before { + content: "\ecf5"; +} +.icon-stop:before { + content: "\ecf6"; +} +.icon-strikethrough-01:before { + content: "\ecf7"; +} +.icon-strikethrough-02:before { + content: "\ecf8"; +} +.icon-strikethrough-square:before { + content: "\ecf9"; +} +.icon-subscript:before { + content: "\ecfa"; +} +.icon-sun-setting-01:before { + content: "\ecfb"; +} +.icon-sun-setting-02:before { + content: "\ecfc"; +} +.icon-sun-setting-03:before { + content: "\ecfd"; +} +.icon-sun:before { + content: "\ecfe"; +} +.icon-sunrise:before { + content: "\ecff"; +} +.icon-sunset:before { + content: "\ed00"; +} +.icon-switch-horizontal-01:before { + content: "\ed01"; +} +.icon-switch-horizontal-02:before { + content: "\ed02"; +} +.icon-switch-vertical-01:before { + content: "\ed03"; +} +.icon-switch-vertical-02:before { + content: "\ed04"; +} +.icon-table:before { + content: "\ed05"; +} +.icon-tablet-01:before { + content: "\ed06"; +} +.icon-tablet-02:before { + content: "\ed07"; +} +.icon-tag-01:before { + content: "\ed08"; +} +.icon-tag-02:before { + content: "\ed09"; +} +.icon-tag-03:before { + content: "\ed0a"; +} +.icon-target-01:before { + content: "\ed0b"; +} +.icon-target-02:before { + content: "\ed0c"; +} +.icon-target-03:before { + content: "\ed0d"; +} +.icon-target-04:before { + content: "\ed0e"; +} +.icon-target-05:before { + content: "\ed0f"; +} +.icon-telescope:before { + content: "\ed10"; +} +.icon-terminal-browser:before { + content: "\ed11"; +} +.icon-terminal-circle:before { + content: "\ed12"; +} +.icon-terminal-square:before { + content: "\ed13"; +} +.icon-terminal:before { + content: "\ed14"; +} +.icon-text-input:before { + content: "\ed15"; +} +.icon-thermometer-01:before { + content: "\ed16"; +} +.icon-thermometer-02:before { + content: "\ed17"; +} +.icon-thermometer-03:before { + content: "\ed18"; +} +.icon-thermometer-cold:before { + content: "\ed19"; +} +.icon-thermometer-warm:before { + content: "\ed1a"; +} +.icon-thumbs-down:before { + content: "\ed1b"; +} +.icon-thumbs-up:before { + content: "\ed1c"; +} +.icon-ticket-01:before { + content: "\ed1d"; +} +.icon-ticket-02:before { + content: "\ed1e"; +} +.icon-toggle-01-left:before { + content: "\ed1f"; +} +.icon-toggle-01-right:before { + content: "\ed20"; +} +.icon-toggle-02-left:before { + content: "\ed21"; +} +.icon-toggle-02-right:before { + content: "\ed22"; +} +.icon-toggle-03-left:before { + content: "\ed23"; +} +.icon-toggle-03-right:before { + content: "\ed24"; +} +.icon-tool-01:before { + content: "\ed25"; +} +.icon-tool-02:before { + content: "\ed26"; +} +.icon-train:before { + content: "\ed27"; +} +.icon-tram:before { + content: "\ed28"; +} +.icon-transform:before { + content: "\ed29"; +} +.icon-translate-01:before { + content: "\ed2a"; +} +.icon-translate-02:before { + content: "\ed2b"; +} +.icon-trash-01:before { + content: "\ed2c"; +} +.icon-trash-02:before { + content: "\ed2d"; +} +.icon-trash-03:before { + content: "\ed2e"; +} +.icon-trash-04:before { + content: "\ed2f"; +} +.icon-trend-down-01:before { + content: "\ed30"; +} +.icon-trend-down-02:before { + content: "\ed31"; +} +.icon-trend-up-01:before { + content: "\ed32"; +} +.icon-trend-up-02:before { + content: "\ed33"; +} +.icon-triangle:before { + content: "\ed34"; +} +.icon-trophy-01:before { + content: "\ed35"; +} +.icon-trophy-02:before { + content: "\ed36"; +} +.icon-truck-01:before { + content: "\ed37"; +} +.icon-truck-02:before { + content: "\ed38"; +} +.icon-tv-01:before { + content: "\ed39"; +} +.icon-tv-02:before { + content: "\ed3a"; +} +.icon-tv-03:before { + content: "\ed3b"; +} +.icon-type-01:before { + content: "\ed3c"; +} +.icon-type-02:before { + content: "\ed3d"; +} +.icon-type-square:before { + content: "\ed3e"; +} +.icon-type-strikethrough-01:before { + content: "\ed3f"; +} +.icon-type-strikethrough-02:before { + content: "\ed40"; +} +.icon-umbrella-01:before { + content: "\ed41"; +} +.icon-umbrella-02:before { + content: "\ed42"; +} +.icon-umbrella-03:before { + content: "\ed43"; +} +.icon-underline-01:before { + content: "\ed44"; +} +.icon-underline-02:before { + content: "\ed45"; +} +.icon-underline-square:before { + content: "\ed46"; +} +.icon-upload-01:before { + content: "\ed47"; +} +.icon-upload-02:before { + content: "\ed48"; +} +.icon-upload-03:before { + content: "\ed49"; +} +.icon-upload-04:before { + content: "\ed4a"; +} +.icon-upload-cloud-01:before { + content: "\ed4b"; +} +.icon-upload-cloud-02:before { + content: "\ed4c"; +} +.icon-usb-flash-drive:before { + content: "\ed4d"; +} +.icon-user-01:before { + content: "\ed4e"; +} +.icon-user-02:before { + content: "\ed4f"; +} +.icon-user-03:before { + content: "\ed50"; +} +.icon-user-check-01:before { + content: "\ed51"; +} +.icon-user-check-02:before { + content: "\ed52"; +} +.icon-user-circle:before { + content: "\ed53"; +} +.icon-user-down-01:before { + content: "\ed54"; +} +.icon-user-down-02:before { + content: "\ed55"; +} +.icon-user-edit:before { + content: "\ed56"; +} +.icon-user-left-01:before { + content: "\ed57"; +} +.icon-user-left-02:before { + content: "\ed58"; +} +.icon-user-minus-01:before { + content: "\ed59"; +} +.icon-user-minus-02:before { + content: "\ed5a"; +} +.icon-user-plus-01:before { + content: "\ed5b"; +} +.icon-user-plus-02:before { + content: "\ed5c"; +} +.icon-user-right-01:before { + content: "\ed5d"; +} +.icon-user-right-02:before { + content: "\ed5e"; +} +.icon-user-square:before { + content: "\ed5f"; +} +.icon-user-up-01:before { + content: "\ed60"; +} +.icon-user-up-02:before { + content: "\ed61"; +} +.icon-user-x-01:before { + content: "\ed62"; +} +.icon-user-x-02:before { + content: "\ed63"; +} +.icon-users-01:before { + content: "\ed64"; +} +.icon-users-02:before { + content: "\ed65"; +} +.icon-users-03:before { + content: "\ed66"; +} +.icon-users-check:before { + content: "\ed67"; +} +.icon-users-down:before { + content: "\ed68"; +} +.icon-users-edit:before { + content: "\ed69"; +} +.icon-users-left:before { + content: "\ed6a"; +} +.icon-users-minus:before { + content: "\ed6b"; +} +.icon-users-plus:before { + content: "\ed6c"; +} +.icon-users-right:before { + content: "\ed6d"; +} +.icon-users-up:before { + content: "\ed6e"; +} +.icon-users-x:before { + content: "\ed6f"; +} +.icon-variable:before { + content: "\ed70"; +} +.icon-video-recorder-off:before { + content: "\ed71"; +} +.icon-video-recorder:before { + content: "\ed72"; +} +.icon-virus:before { + content: "\ed73"; +} +.icon-voicemail:before { + content: "\ed74"; +} +.icon-volume-max:before { + content: "\ed75"; +} +.icon-volume-min:before { + content: "\ed76"; +} +.icon-volume-minus:before { + content: "\ed77"; +} +.icon-volume-plus:before { + content: "\ed78"; +} +.icon-volume-x:before { + content: "\ed79"; +} +.icon-wallet-01:before { + content: "\ed7a"; +} +.icon-wallet-02:before { + content: "\ed7b"; +} +.icon-wallet-03:before { + content: "\ed7c"; +} +.icon-wallet-04:before { + content: "\ed7d"; +} +.icon-wallet-05:before { + content: "\ed7e"; +} +.icon-watch-circle:before { + content: "\ed7f"; +} +.icon-watch-square:before { + content: "\ed80"; +} +.icon-waves:before { + content: "\ed81"; +} +.icon-webcam-01:before { + content: "\ed82"; +} +.icon-webcam-02:before { + content: "\ed83"; +} +.icon-wifi-off:before { + content: "\ed84"; +} +.icon-wifi:before { + content: "\ed85"; +} +.icon-wind-01:before { + content: "\ed86"; +} +.icon-wind-02:before { + content: "\ed87"; +} +.icon-wind-03:before { + content: "\ed88"; +} +.icon-x-circle:before { + content: "\ed89"; +} +.icon-x-close:before { + content: "\ed8a"; +} +.icon-x-square:before { + content: "\ed8b"; +} +.icon-x:before { + content: "\ed8c"; +} +.icon-youtube:before { + content: "\ed8d"; +} +.icon-zap-circle:before { + content: "\ed8e"; +} +.icon-zap-fast:before { + content: "\ed8f"; +} +.icon-zap-off:before { + content: "\ed90"; +} +.icon-zap-square:before { + content: "\ed91"; +} +.icon-zap:before { + content: "\ed92"; +} +.icon-zoom-in:before { + content: "\ed93"; +} +.icon-zoom-out:before { + content: "\ed94"; +} diff --git a/src/seqta/ui/AddBetterSEQTAElements.ts b/src/seqta/ui/AddBetterSEQTAElements.ts index c09637a3..7edab9f3 100644 --- a/src/seqta/ui/AddBetterSEQTAElements.ts +++ b/src/seqta/ui/AddBetterSEQTAElements.ts @@ -13,11 +13,11 @@ export async function AddBetterSEQTAElements() { document.documentElement.classList.add('dark'); } createHomeButton(); + await appendBackgroundToUI(); await handleUserInfo(); handleStudentData(); createNewsButton(); setupEventListeners(); - appendBackgroundToUI(); await addDarkLightToggle(); customizeMenuToggle(); } diff --git a/src/seqta/ui/ImageBackgrounds.ts b/src/seqta/ui/ImageBackgrounds.ts index 71a6a0f1..449914df 100644 --- a/src/seqta/ui/ImageBackgrounds.ts +++ b/src/seqta/ui/ImageBackgrounds.ts @@ -1,13 +1,79 @@ -import browser from 'webextension-polyfill'; +import { getDataById, isIndexedDBSupported } from '@/interface/hooks/BackgroundDataLoader'; export async function appendBackgroundToUI() { const parent = document.getElementById('container'); + if (!parent) return; - // embed background.html - const background = document.createElement('iframe'); - background.id = 'background'; - background.classList.add('imageBackground'); - background.setAttribute('excludeDarkCheck', 'true'); - background.src = browser.runtime.getURL('seqta/ui/background/background.html'); - parent!.appendChild(background); + const backgroundContainer = document.createElement('div'); + backgroundContainer.classList.add('imageBackground'); + backgroundContainer.setAttribute('excludeDarkCheck', 'true'); + + const mediaContainer = document.createElement('div'); + mediaContainer.id = 'media-container'; + backgroundContainer.appendChild(mediaContainer); + + parent.appendChild(backgroundContainer); + await loadBackground(); + return; } + +export async function loadBackground() { + if (!isIndexedDBSupported()) { + console.error("IndexedDB is not supported. Unable to load background."); + return; + } + + try { + const selectedBackgroundId = localStorage.getItem('selectedBackground'); + if (!selectedBackgroundId) { + const backgroundContainer = document.querySelector('.imageBackground'); + if (backgroundContainer) { + backgroundContainer.remove(); + } + return; + } + + const background = await getDataById(selectedBackgroundId); + if (!background) return; + + let backgroundContainer = document.querySelector('.imageBackground'); + if (!backgroundContainer) { + backgroundContainer = document.createElement('div'); + backgroundContainer.classList.add('imageBackground'); + backgroundContainer.setAttribute('excludeDarkCheck', 'true'); + const parent = document.getElementById('container'); + if (parent) { + parent.appendChild(backgroundContainer); + } + } + + let mediaContainer = document.getElementById('media-container'); + if (!mediaContainer) { + mediaContainer = document.createElement('div'); + mediaContainer.id = 'media-container'; + backgroundContainer.appendChild(mediaContainer); + } + + mediaContainer = document.getElementById('media-container'); + if (!mediaContainer) return; + + mediaContainer.innerHTML = ''; + + const mediaElement = background.type === 'video' + ? document.createElement('video') + : document.createElement('img'); + + mediaElement.src = URL.createObjectURL(background.blob); + mediaElement.classList.add('background'); + + if (mediaElement instanceof HTMLVideoElement) { + mediaElement.loop = true; + mediaElement.muted = true; + mediaElement.autoplay = true; + } + + mediaContainer.appendChild(mediaElement); + } catch (error) { + console.error('Error loading background:', error); + } +} \ No newline at end of file diff --git a/src/seqta/ui/ThemeCreator.ts b/src/seqta/ui/ThemeCreator.ts index efb47cf9..8cfde301 100644 --- a/src/seqta/ui/ThemeCreator.ts +++ b/src/seqta/ui/ThemeCreator.ts @@ -1,73 +1,80 @@ -import browser from "webextension-polyfill"; +import renderSvelte from "@/interface/main" +import themeCreator from "@/interface/pages/themeCreator.svelte" +import { unmount } from "svelte" +import { ClearThemePreview } from "./themes/UpdateThemePreview" + +let themeCreatorSvelteApp: any = null /** * Open the Theme Creator sidebar, it is an embedded page loaded similar to the extension popup * @param themeID - The ID of the theme to load in the Theme Creator * @returns void */ -export function OpenThemeCreator( themeID: string = '' ) { - CloseThemeCreator(); - - const width = '310px'; +export function OpenThemeCreator(themeID: string = "") { + CloseThemeCreator() - const themeCreatorIframe: HTMLIFrameElement = document.createElement('iframe'); - themeCreatorIframe.src = `${browser.runtime.getURL('interface/index.html')}${ themeID != '' ? `?themeID=${themeID}` : '' }#themeCreator`; - themeCreatorIframe.id = 'themeCreatorIframe'; - themeCreatorIframe.setAttribute('allowTransparency', 'true'); - themeCreatorIframe.setAttribute('excludeDarkCheck', 'true'); - themeCreatorIframe.style.border = 'none'; - themeCreatorIframe.style.width = width; + const width = "310px" - const mainContent = document.querySelector('#container') as HTMLDivElement; - if (mainContent) mainContent.style.width = `calc(100% - ${width})`; + const themeCreatorDiv: HTMLDivElement = document.createElement("div") + themeCreatorDiv.id = "themeCreator" + themeCreatorDiv.style.width = width + + const shadow = themeCreatorDiv.attachShadow({ mode: "open" }) + themeCreatorSvelteApp = renderSvelte(themeCreator, shadow, { + themeID: themeID, + }) + + const mainContent = document.querySelector("#container") as HTMLDivElement + if (mainContent) mainContent.style.width = `calc(100% - ${width})` // close button - const closeButton = document.createElement('button'); - closeButton.classList.add('themeCloseButton'); - closeButton.textContent = '×'; - closeButton.addEventListener('click', CloseThemeCreator); - document.body.appendChild(closeButton); + const closeButton = document.createElement("button") + closeButton.classList.add("themeCloseButton") + closeButton.textContent = "×" + closeButton.addEventListener("click", () => { + CloseThemeCreator() + ClearThemePreview() + }) - const resizeBar = document.createElement('div'); - resizeBar.classList.add('resizeBar'); - resizeBar.style.right = '307.5px'; + document.body.appendChild(closeButton) - let isDragging = false; - let currentX: number; + const resizeBar = document.createElement("div") + resizeBar.classList.add("resizeBar") + resizeBar.style.right = "307.5px" - const mouseDownHandler = (e: MouseEvent) => { - isDragging = true; - currentX = e.clientX; - document.addEventListener('mousemove', mouseMoveHandler); - document.addEventListener('mouseup', mouseUpHandler); - document.body.style.userSelect = 'none'; - themeCreatorIframe.style.pointerEvents = 'none'; // Disable pointer events on iframe during resize - }; + let isDragging = false + + const mouseDownHandler = (_: MouseEvent) => { + isDragging = true + document.addEventListener("mousemove", mouseMoveHandler) + document.addEventListener("mouseup", mouseUpHandler) + document.body.style.userSelect = "none" + themeCreatorDiv.style.pointerEvents = "none" + } const mouseMoveHandler = (e: MouseEvent) => { - if (!isDragging) return; - const dx = e.clientX - currentX; - currentX = e.clientX; - const newWidth = Math.min(Math.max(310, themeCreatorIframe.offsetWidth - dx), 600); - themeCreatorIframe.style.width = `${newWidth}px`; - mainContent.style.width = `calc(100% - ${newWidth}px)`; - resizeBar.style.right = `${newWidth - 2.5}px`; - }; + if (!isDragging) return + const windowWidth = window.innerWidth + const newWidth = Math.min(Math.max(310, windowWidth - e.clientX), 600) + themeCreatorDiv.style.width = `${newWidth}px` + mainContent.style.width = `calc(100% - ${newWidth}px)` + resizeBar.style.right = `${newWidth - 2.5}px` + } const mouseUpHandler = () => { - isDragging = false; - document.removeEventListener('mousemove', mouseMoveHandler); - document.removeEventListener('mouseup', mouseUpHandler); - document.body.style.userSelect = ''; - themeCreatorIframe.style.pointerEvents = 'auto'; - }; + isDragging = false + document.removeEventListener("mousemove", mouseMoveHandler) + document.removeEventListener("mouseup", mouseUpHandler) + document.body.style.userSelect = "" + themeCreatorDiv.style.pointerEvents = "auto" + } - resizeBar.addEventListener('mousedown', mouseDownHandler); - resizeBar.addEventListener('mouseover', () => resizeBar.style.opacity = '1'); - resizeBar.addEventListener('mouseout', () => resizeBar.style.opacity = '0'); + resizeBar.addEventListener("mousedown", mouseDownHandler) + resizeBar.addEventListener("mouseover", () => (resizeBar.style.opacity = "1")) + resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0")) - document.body.appendChild(themeCreatorIframe); - document.body.appendChild(resizeBar); + document.body.appendChild(themeCreatorDiv) + document.body.appendChild(resizeBar) } /** @@ -75,14 +82,17 @@ export function OpenThemeCreator( themeID: string = '' ) { * @returns void */ export function CloseThemeCreator() { - const themeCreatorIframe = document.getElementById('themeCreatorIframe'); - const closeButton = document.querySelector('.themeCloseButton') as HTMLButtonElement; - const resizeBar = document.querySelector('.resizeBar') as HTMLDivElement; - - if (themeCreatorIframe) themeCreatorIframe.remove(); - if (closeButton) closeButton.remove(); - if (resizeBar) resizeBar.remove(); + const themeCreator = document.getElementById("themeCreator") + const closeButton = document.querySelector( + ".themeCloseButton", + ) as HTMLButtonElement + const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement - const mainContent = document.querySelector('#container') as HTMLDivElement; - if (mainContent) mainContent.style.width = '100%'; -} \ No newline at end of file + if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp) + if (themeCreator) themeCreator.remove() + if (closeButton) closeButton.remove() + if (resizeBar) resizeBar.remove() + + const mainContent = document.querySelector("#container") as HTMLDivElement + if (mainContent) mainContent.style.width = "100%" +} diff --git a/src/seqta/ui/background/background.html b/src/seqta/ui/background/background.html deleted file mode 100644 index c78dcf96..00000000 --- a/src/seqta/ui/background/background.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Background Fetcher - - - - - -
    - - - - diff --git a/src/seqta/ui/background/background.ts b/src/seqta/ui/background/background.ts deleted file mode 100644 index c76c0f75..00000000 --- a/src/seqta/ui/background/background.ts +++ /dev/null @@ -1,115 +0,0 @@ -interface Data { - blob: Blob; - type: 'image' | 'video'; -} - -interface DatabaseEventTarget extends EventTarget { - result: IDBDatabase; -} - -interface DatabaseEvent extends Event { - target: DatabaseEventTarget; -} - -const openDB = (): Promise => { - return new Promise((resolve, reject) => { - const request = indexedDB.open('MyDatabase', 1); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - - request.onupgradeneeded = (event: IDBVersionChangeEvent) => { - // @ts-expect-error - The event type is not recognized by TypeScript - event?.target?.result.createObjectStore('backgrounds', { keyPath: 'id' }); - }; - }); -}; - -const readData = async (): Promise => { - const selectedBackground = localStorage.getItem('selectedBackground'); - - //const selectedBackground = localStorage.getItem('selectedBackground'); - if (!selectedBackground || selectedBackground === '') { - return null; - } - - const db = await openDB(); - const tx = db.transaction('backgrounds', 'readonly'); - const store = tx.objectStore('backgrounds'); - const request = store.get(selectedBackground); - - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result as Data); - request.onerror = () => reject(request.error); - }); -}; - -const updateBackground = async (): Promise => { - try { - const data = await readData(); - if (!data) { - const container = document.getElementById('media-container'); - const currentMedia = container?.querySelector('.current-media'); - if (currentMedia) { - currentMedia.remove(); - } - return; - } - - const url = URL.createObjectURL(data.blob); - const container = document.getElementById('media-container'); - - // Create new element and set properties - let newElement; - if (data.type === 'image') { - newElement = document.createElement('img'); - newElement.src = url; - newElement.alt = 'Uploaded content'; - } else if (data.type === 'video') { - newElement = document.createElement('video'); - newElement.src = url; - newElement.autoplay = true; - newElement.loop = true; - newElement.muted = true; - } - - // Mark the old element for removal - const oldElement = container?.querySelector('.current-media'); - if (oldElement) { - oldElement.classList.remove('current-media'); - oldElement.classList.add('old-media'); - } - - // Add the new element and mark it as current - newElement?.classList.add('current-media'); - container?.appendChild(newElement as Node); - - // Delay removal of old element - setTimeout(() => { - const oldMedia = container?.querySelector('.old-media'); - if (oldMedia) { - oldMedia.remove(); - } - }, 100); // 0.1 second delay - } catch (error) { - console.error('An error occurred:', error); - } -}; - -// Main function to run on page load -const main = async (): Promise => { - await updateBackground(); - - // Listen for changes to local storage - try { - window.addEventListener('storage', async (event) => { - if (event.key === 'selectedBackground') { - await updateBackground(); - } - }); - } catch (error) { - console.error('An error occurred:', error); - } -}; - -main() \ No newline at end of file diff --git a/src/seqta/ui/renderStore.ts b/src/seqta/ui/renderStore.ts new file mode 100644 index 00000000..7a20fa13 --- /dev/null +++ b/src/seqta/ui/renderStore.ts @@ -0,0 +1,35 @@ +import renderSvelte from '@/interface/main'; +import Store from '@/interface/pages/store.svelte' + +import { unmount } from 'svelte' + +let remove: () => void + +export function OpenStorePage() { + remove = renderStore() +} + +export function renderStore() { + const container = document.querySelector('#container'); + if (!container) { + throw new Error('Container not found'); + } + + const child = document.createElement('div'); + child.id = 'store'; + container!.appendChild(child); + + const shadow = child.attachShadow({ mode: 'open' }); + const app = renderSvelte(Store, shadow); + + return () => unmount(app) +} + +export function closeStore() { + document.getElementById('store')!.classList.add('hide') + + setTimeout(() => { + remove() + document.getElementById('store')!.remove() + }, 500) +} diff --git a/src/seqta/ui/themes/Themes.ts b/src/seqta/ui/themes/Themes.ts index 1462ebef..cd309d2b 100644 --- a/src/seqta/ui/themes/Themes.ts +++ b/src/seqta/ui/themes/Themes.ts @@ -1,17 +1,5 @@ -import { base64toblobURL } from '@/seqta/utils/imageConversions'; - export const imageData: Record = {}; -export const UpdateImageData = (image: { id: string; base64: string }) => { - const { id, base64 } = image; - - if (imageData[id]) { - imageData[id].url = base64toblobURL(base64); - const { variableName } = imageData[id]; - document.documentElement.style.setProperty('--' + variableName, `url(${imageData[id].url})`); - } -}; - export function applyCustomCSS(customCSS: string) { let styleElement = document.getElementById('custom-theme'); if (!styleElement) { diff --git a/src/seqta/ui/themes/UpdateThemePreview.ts b/src/seqta/ui/themes/UpdateThemePreview.ts index b60aebc0..288ded87 100644 --- a/src/seqta/ui/themes/UpdateThemePreview.ts +++ b/src/seqta/ui/themes/UpdateThemePreview.ts @@ -1,65 +1,75 @@ -import { CustomThemeBase64 } from '@/interface/types/CustomThemes'; -import { applyCustomCSS, imageData, removeImageFromDocument, UpdateImageData } from './Themes'; +import type { LoadedCustomTheme } from '@/types/CustomThemes'; +import { applyCustomCSS, removeImageFromDocument } from './Themes'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; +let previousImageVariableNames: string[] = []; +let originalColor: string | null = null; +let originalTheme: boolean | null = null; -export const UpdateThemePreview = async (updatedTheme: CustomThemeBase64 /* Omit & { CustomImages: Omit[] } */) => { - const { CustomCSS, CustomImages, defaultColour } = updatedTheme; +export const UpdateThemePreview = async (updatedTheme: LoadedCustomTheme) => { + const { CustomCSS, CustomImages, defaultColour, forceDark } = updatedTheme; - if (updatedTheme.forceDark != undefined) { - if (updatedTheme.forceDark) { - settingsState.DarkMode = true; - } else { - settingsState.DarkMode = false; + // Update dark mode setting + if (forceDark !== undefined) { + // Store the original theme if it hasn't been stored yet + if (originalTheme === null) { + originalTheme = settingsState.DarkMode; } + settingsState.DarkMode = forceDark; } - // Update image data - const currentImageIds = Object.keys(imageData); - const updatedImageIds = CustomImages.map((image) => image.id); + // Get the new image variable names + const newImageVariableNames = CustomImages.map(image => image.variableName); - // Remove unused images from imageData and document - currentImageIds.forEach((imageId) => { - if (!updatedImageIds.includes(imageId)) { - const { variableName } = imageData[imageId]; + // Remove images that are no longer present + previousImageVariableNames.forEach(variableName => { + if (!newImageVariableNames.includes(variableName)) { removeImageFromDocument(variableName); - delete imageData[imageId]; } }); - // Update or add new images to imageData - CustomImages.forEach((image) => { - const existingImage = imageData[image.id]; - - if (existingImage && existingImage.variableName !== image.variableName) { - // Remove the previous variableName from the document - removeImageFromDocument(existingImage.variableName); - - // Update the variableName in imageData - imageData[image.id].variableName = image.variableName; - - // Update the variableName in the document - document.documentElement.style.setProperty('--' + image.variableName, `url(${existingImage.url})`); - } - - if (image.url) { - UpdateImageData({ - id: image.id, - base64: image.url - }); - } - - imageData[image.id] = { - url: imageData[image.id]?.url || '', - variableName: image.variableName, - }; + // Update or add new images + CustomImages.forEach((image: any) => { + document.documentElement.style.setProperty(`--${image.variableName}`, `url(${image.url})`); }); + // Update the previousImageVariableNames for the next run + previousImageVariableNames = newImageVariableNames; + // Apply custom CSS applyCustomCSS(CustomCSS); // Apply default color - if (defaultColour !== '') { - settingsState.selectedColor = defaultColour + if (defaultColour) { + // Store the original color if it hasn't been stored yet + if (originalColor === null) { + originalColor = settingsState.selectedColor; + } + settingsState.selectedColor = defaultColour; } }; + +export const ClearThemePreview = () => { + previousImageVariableNames.forEach(variableName => { + removeImageFromDocument(variableName); + }); + + previousImageVariableNames = []; + + let styleElement = document.getElementById('custom-theme'); + if (styleElement) { + styleElement.remove(); + } + + // Reset the color to the original value + if (originalColor !== null) { + settingsState.selectedColor = originalColor; + originalColor = null; + } + + // Reset the theme (dark/light mode) to the original value + if (originalTheme !== null) { + settingsState.DarkMode = originalTheme; + originalTheme = null; + } +} \ No newline at end of file diff --git a/src/seqta/ui/themes/applyTheme.ts b/src/seqta/ui/themes/applyTheme.ts index 7e2fae0f..04f75bb1 100644 --- a/src/seqta/ui/themes/applyTheme.ts +++ b/src/seqta/ui/themes/applyTheme.ts @@ -1,4 +1,4 @@ -import { CustomImage, CustomTheme } from '@/interface/types/CustomThemes'; +import type { CustomImage, CustomTheme } from '@/types/CustomThemes'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import { applyCustomCSS } from './Themes'; diff --git a/src/seqta/ui/themes/deleteTheme.ts b/src/seqta/ui/themes/deleteTheme.ts index 2f9d1a38..1ced7a77 100644 --- a/src/seqta/ui/themes/deleteTheme.ts +++ b/src/seqta/ui/themes/deleteTheme.ts @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { CustomTheme } from '@/interface/types/CustomThemes'; +import type { CustomTheme } from '@/types/CustomThemes'; import { removeTheme } from './removeTheme'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; diff --git a/src/seqta/ui/themes/disableTheme.ts b/src/seqta/ui/themes/disableTheme.ts index cebd5983..f033571f 100644 --- a/src/seqta/ui/themes/disableTheme.ts +++ b/src/seqta/ui/themes/disableTheme.ts @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { CustomTheme } from '@/interface/types/CustomThemes'; +import type { CustomTheme } from '@/types/CustomThemes'; import { removeTheme } from './removeTheme'; import { Mutex } from '@/seqta/utils/mutex'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; @@ -8,11 +8,10 @@ const mutex = new Mutex(); let isDisabling = false; export const disableTheme = async () => { - console.log('Disabling theme', isDisabling) if (isDisabling) return; if (!settingsState.selectedTheme || settingsState.selectedTheme === '') { - console.log('Theme is already disabled, exit early') + console.debug('Theme is already disabled, exit early') // Theme is already disabled, exit early return; } @@ -20,7 +19,7 @@ export const disableTheme = async () => { const unlock = await mutex.lock(); try { if (settingsState.selectedTheme) { - console.log('Disabling theme:', settingsState.selectedTheme); + console.debug('Disabling theme:', settingsState.selectedTheme); const theme = await localforage.getItem(settingsState.selectedTheme) as CustomTheme; if (theme) { await removeTheme(theme); diff --git a/src/seqta/ui/themes/downloadTheme.ts b/src/seqta/ui/themes/downloadTheme.ts index 83ea5623..630cb624 100644 --- a/src/seqta/ui/themes/downloadTheme.ts +++ b/src/seqta/ui/themes/downloadTheme.ts @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { Theme } from '@/interface/pages/Store'; +import type { Theme } from '@/old-interface/pages/Store'; import base64ToBlob from '@/seqta/utils/base64ToBlob'; type ThemeContent = { @@ -31,13 +31,13 @@ export const InstallTheme = async (themeData: ThemeContent) => { blob: base64ToBlob(image.data) })); - let availableThemes = await localforage.getItem('availableThemes') as string[]; + let availableThemes = await localforage.getItem('customThemes') as string[]; if (availableThemes && !availableThemes.includes(themeData.id)) { availableThemes.push(themeData.id); } else if (!availableThemes) { availableThemes = [themeData.id]; } - await localforage.setItem('availableThemes', availableThemes); + await localforage.setItem('customThemes', availableThemes); await localforage.setItem(themeData.id, { ...themeData, diff --git a/src/seqta/ui/themes/enableCurrent.ts b/src/seqta/ui/themes/enableCurrent.ts index 40379d2e..6a6a0374 100644 --- a/src/seqta/ui/themes/enableCurrent.ts +++ b/src/seqta/ui/themes/enableCurrent.ts @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { CustomTheme } from '@/interface/types/CustomThemes'; +import type { CustomTheme } from '@/types/CustomThemes'; import { applyTheme } from './applyTheme'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; diff --git a/src/seqta/ui/themes/getAvailableThemes.ts b/src/seqta/ui/themes/getAvailableThemes.ts index 429c04a3..e2d1d464 100644 --- a/src/seqta/ui/themes/getAvailableThemes.ts +++ b/src/seqta/ui/themes/getAvailableThemes.ts @@ -1,20 +1,15 @@ import localforage from 'localforage'; -import { CustomTheme, ThemeList } from '@/interface/types/CustomThemes'; -import { blobToBase64 } from '@/seqta/utils/blobToBase64'; +import type { CustomTheme, ThemeList } from '@/types/CustomThemes'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; -export const getAvailableThemes = async (): Promise => { +export const getAvailableThemes = async (): Promise => { try { const themeIds = await localforage.getItem('customThemes') as string[] | null; if (themeIds) { const themes = await Promise.all( themeIds.map(async (id) => { const theme = await localforage.getItem(id) as CustomTheme; - const { CustomImages, ...themeWithoutImages } = theme; - return { - ...themeWithoutImages, - coverImage: theme.coverImage ? await blobToBase64(theme.coverImage as Blob) : '', - }; + return theme; }) ); diff --git a/src/seqta/ui/themes/getTheme.ts b/src/seqta/ui/themes/getTheme.ts index c698e13e..d608cfb4 100644 --- a/src/seqta/ui/themes/getTheme.ts +++ b/src/seqta/ui/themes/getTheme.ts @@ -1,28 +1,12 @@ import localforage from 'localforage'; -import { CustomImageBase64, CustomTheme, CustomThemeBase64 } from '@/interface/types/CustomThemes'; -import { blobToBase64 } from '@/seqta/utils/blobToBase64'; +import type { LoadedCustomTheme } from '@/types/CustomThemes'; -export const getTheme = async (themeId: string): Promise => { +export const getTheme = async (themeId: string): Promise => { try { - const theme = await localforage.getItem(themeId) as CustomTheme; + const theme = await localforage.getItem(themeId) as LoadedCustomTheme; - const CustomImages: CustomImageBase64[] = await Promise.all( - theme.CustomImages.map(async (image) => { - const base64 = await blobToBase64(image.blob); - return { - id: image.id, - variableName: image.variableName, - url: base64, - }; - }) - ); - - return { - ...theme, - coverImage: theme.coverImage ? await blobToBase64(theme.coverImage as Blob) : null, - CustomImages, - }; + return theme; } catch (error) { console.error('Error getting theme:', error); return null; diff --git a/src/seqta/ui/themes/removeTheme.ts b/src/seqta/ui/themes/removeTheme.ts index bfb5a613..ef3a1e37 100644 --- a/src/seqta/ui/themes/removeTheme.ts +++ b/src/seqta/ui/themes/removeTheme.ts @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { CustomTheme } from '@/interface/types/CustomThemes'; +import type { CustomTheme } from '@/types/CustomThemes'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; export const removeTheme = async (theme: CustomTheme) => { diff --git a/src/seqta/ui/themes/saveTheme.ts b/src/seqta/ui/themes/saveTheme.ts index 060eaa6b..9ae3f094 100644 --- a/src/seqta/ui/themes/saveTheme.ts +++ b/src/seqta/ui/themes/saveTheme.ts @@ -1,39 +1,29 @@ import localforage from 'localforage'; -import { CustomTheme, CustomThemeBase64 } from '@/interface/types/CustomThemes'; +import type { LoadedCustomTheme } from '@/types/CustomThemes'; import { disableTheme } from './disableTheme'; +import { themeUpdates } from '@/interface/hooks/ThemeUpdates'; -export const saveTheme = async (theme: CustomThemeBase64) => { +export const saveTheme = async (theme: LoadedCustomTheme) => { try { - const updatedTheme: CustomTheme = { - ...theme, - coverImage: theme.coverImage ? await fetch(theme.coverImage).then((res) => res.blob()) : null, - CustomImages: await Promise.all( - theme.CustomImages.map(async (image) => ({ - id: image.id, - blob: await fetch(image.url).then((res) => res.blob()), - variableName: image.variableName, - })) - ), - }; - disableTheme(); - console.debug('Theme to save:', updatedTheme); - - await localforage.setItem(updatedTheme.id, updatedTheme); + console.debug('Theme to save:', theme); + + await localforage.setItem(theme.id, theme); await localforage.getItem('customThemes').then((themes: unknown) => { const themeList = themes as string[] | null; if (themeList) { - if (!themeList.includes(updatedTheme.id)) { - themeList.push(updatedTheme.id); + if (!themeList.includes(theme.id)) { + themeList.push(theme.id); localforage.setItem('customThemes', themeList); } } else { - localforage.setItem('customThemes', [updatedTheme.id]); + localforage.setItem('customThemes', [theme.id]); } }); console.debug('Theme saved successfully!'); + themeUpdates.triggerUpdate(); } catch (error) { console.error('Error saving theme:', error); } diff --git a/src/seqta/ui/themes/setTheme.ts b/src/seqta/ui/themes/setTheme.ts index 0646408a..6e560cea 100644 --- a/src/seqta/ui/themes/setTheme.ts +++ b/src/seqta/ui/themes/setTheme.ts @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { CustomTheme } from '@/interface/types/CustomThemes'; +import type { CustomTheme } from '@/types/CustomThemes'; import { applyTheme } from './applyTheme'; import { removeTheme } from './removeTheme'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; diff --git a/src/seqta/ui/themes/shareTheme.ts b/src/seqta/ui/themes/shareTheme.ts index 0d7b6454..445d98e2 100644 --- a/src/seqta/ui/themes/shareTheme.ts +++ b/src/seqta/ui/themes/shareTheme.ts @@ -6,7 +6,7 @@ const saveThemeFile = (data: object, fileName: string) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${fileName}.json`; + a.download = `${fileName}.json.theme`; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -34,13 +34,14 @@ const shareTheme = async (themeID: string) => { }); // Convert cover image to Base64 - const coverImageBlob = await fetch(coverImage as string).then(res => res.blob()); - const coverImageBase64 = await blobToBase64(coverImageBlob); + let coverImageBase64 = null; + if (coverImage) { + coverImageBase64 = await blobToBase64(coverImage); + } // Convert custom images to Base64 const finalImages = await Promise.all(CustomImages.map(async (image) => { - const imageBlob = await fetch(image.url as string).then(res => res.blob()); - const imageBase64 = await blobToBase64(imageBlob); + const imageBase64 = await blobToBase64(image.blob); return { id: image.id, variableName: image.variableName, diff --git a/src/seqta/utils/base64ToBlob.ts b/src/seqta/utils/base64ToBlob.ts index 00410d4f..3a428e3b 100644 --- a/src/seqta/utils/base64ToBlob.ts +++ b/src/seqta/utils/base64ToBlob.ts @@ -1,5 +1,5 @@ const base64ToBlob = (base64: string, contentType: string = ''): Blob => { - const byteCharacters = atob(base64.split(',')[1]); + const byteCharacters = atob(base64); const byteArrays: Uint8Array[] = []; for (let offset = 0; offset < byteCharacters.length; offset += 512) { diff --git a/src/seqta/utils/listeners/MessageListener.ts b/src/seqta/utils/listeners/MessageListener.ts index b4d53146..08d1b012 100644 --- a/src/seqta/utils/listeners/MessageListener.ts +++ b/src/seqta/utils/listeners/MessageListener.ts @@ -1,6 +1,6 @@ import browser from 'webextension-polyfill' -import { closeSettings, MenuOptionsOpen, OpenMenuOptions, OpenWhatsNewPopup } from '../../../SEQTA'; +import { closeExtensionPopup, MenuOptionsOpen, OpenMenuOptions } from '../../../SEQTA'; import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'; import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'; import { saveTheme } from '@/seqta/ui/themes/saveTheme'; @@ -23,7 +23,7 @@ export class MessageHandler { switch (request.info) { case 'EditSidebar': this.editSidebar(); - closeSettings(); + closeExtensionPopup(); sendResponse({ status: 'success' }); break; @@ -73,17 +73,11 @@ export class MessageHandler { sendResponse(themes); }); return true; - - case 'OpenChangelog': - OpenWhatsNewPopup(); - closeSettings(); - sendResponse({ status: 'success' }); - break; - + case 'OpenThemeCreator': const themeID = request?.body?.themeID; OpenThemeCreator( themeID ? themeID : '' ); - closeSettings(); + closeExtensionPopup(); sendResponse({ status: 'success' }); break; @@ -116,7 +110,7 @@ export class MessageHandler { editSidebar() { if (!MenuOptionsOpen) { OpenMenuOptions(); - closeSettings(); + closeExtensionPopup(); } } } \ No newline at end of file diff --git a/src/seqta/utils/listeners/SettingsState.ts b/src/seqta/utils/listeners/SettingsState.ts index ad46b139..a3280c42 100644 --- a/src/seqta/utils/listeners/SettingsState.ts +++ b/src/seqta/utils/listeners/SettingsState.ts @@ -1,16 +1,21 @@ import browser from 'webextension-polyfill'; -import { SettingsState } from '@/types/storage'; +import type { SettingsState } from '@/types/storage'; +import type { Subscriber, Unsubscriber } from 'svelte/store'; type ChangeListener = (newValue: any, oldValue: any) => void; +type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void; class StorageManager { private static instance: StorageManager; private data: SettingsState; private listeners: { [key: string]: ChangeListener[] }; + private globalListeners: GlobalChangeListener[]; + private subscribers: Set> = new Set(); private constructor() { this.data = {} as SettingsState; this.listeners = {}; + this.globalListeners = []; this.loadFromStorage(); const handler: ProxyHandler = { @@ -58,6 +63,11 @@ class StorageManager { return instance; } + public setKey(key: K, value: SettingsState[K]): void { + this.data[key] = value; + this.saveToStorage(); + } + private async loadFromStorage(): Promise { const result = await browser.storage.local.get(); this.data = { ...this.data, ...result }; @@ -65,6 +75,7 @@ class StorageManager { private async saveToStorage(): Promise { await browser.storage.local.set(this.data); + this.notifySubscribers(); } private async removeFromStorage(key: string): Promise { @@ -85,6 +96,9 @@ class StorageManager { listener(newValue, oldValue); } } + for (const listener of this.globalListeners) { + listener(newValue, oldValue, key); + } } } }); @@ -101,6 +115,36 @@ class StorageManager { } this.listeners[prop].push(listener); } + + /** + * Register a listener for any setting. + * @param listener The listener to call when any setting changes -> takes two arguments, (newValue, oldValue) + */ + public registerGlobal(listener: GlobalChangeListener): void { + this.globalListeners.push(listener); + } + + /** + * Get all settings. + * @returns All settings. + */ + public getAll(): SettingsState { + return this.data; + } + + public subscribe(run: Subscriber): Unsubscriber { + this.subscribers.add(run); + run(this.data); + return () => { + this.subscribers.delete(run); + }; + } + + private notifySubscribers(): void { + for (const subscriber of this.subscribers) { + subscriber(this.data); + } + } } export const settingsState = StorageManager.getInstance(); diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index 271cd2b9..8f35ee91 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -12,7 +12,7 @@ import { } from '@/SEQTA'; import { updateBgDurations } from '@/seqta/ui/Animation'; import browser from 'webextension-polyfill'; -import { CustomShortcut } from '@/types/storage'; +import type { CustomShortcut } from '@/types/storage'; export class StorageChangeHandler { constructor() { diff --git a/src/seqta/utils/migrateBackgrounds.ts b/src/seqta/utils/migrateBackgrounds.ts new file mode 100644 index 00000000..f2b36694 --- /dev/null +++ b/src/seqta/utils/migrateBackgrounds.ts @@ -0,0 +1,127 @@ +import browser from 'webextension-polyfill'; +import base64ToBlob from './base64ToBlob'; +import { openDatabase, writeData } from '@/interface/hooks/BackgroundDataLoader'; +import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'; +import { loadBackground } from '@/seqta/ui/ImageBackgrounds'; + +const MIGRATION_STATE_KEY = 'background_migration_state'; + +interface MigrationState { + lastProcessedId: string | null; + total: number; + processed: number; + completed: boolean; +} + +export const migrateBackgrounds = async (): Promise => { + console.info('Migrating backgrounds...'); + + const savedState = localStorage.getItem(MIGRATION_STATE_KEY); + const migrationState: MigrationState = savedState + ? JSON.parse(savedState) + : { lastProcessedId: null, total: 0, processed: 0, completed: false }; + + if (migrationState.completed) { + console.info('Migration already completed'); + return; + } + + return new Promise((resolve, reject) => { + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + + const handleMessage = async (event: MessageEvent) => { + if (event.source !== iframe.contentWindow) return; + + switch (event.data.type) { + case 'GET_LAST_PROCESSED_ID': + iframe.contentWindow?.postMessage({ + type: 'LAST_PROCESSED_ID', + id: migrationState.lastProcessedId + }, '*'); + break; + + case 'BACKGROUND_DATA': + try { + const { id, data, mediaType, total, processed, isSelected } = event.data.payload; + const mimeType = mediaType === 'image' ? 'image/png' : 'video/mp4'; + const blob = base64ToBlob(data, mimeType); + + await storeBackground({ + id, + blob, + type: mediaType + }); + + if (isSelected) { + localStorage.setItem('selectedBackground', id); + await loadBackground(); + } + + migrationState.lastProcessedId = id; + migrationState.total = total; + migrationState.processed = processed; + localStorage.setItem(MIGRATION_STATE_KEY, JSON.stringify(migrationState)); + + console.log(`Migrated background: ${id} (${processed}/${total})`); + } catch (error) { + console.error('Error handling background data:', error); + } + break; + + case 'MIGRATION_COMPLETE': + console.info('Migration completed successfully'); + migrationState.completed = true; + localStorage.setItem(MIGRATION_STATE_KEY, JSON.stringify(migrationState)); + window.removeEventListener('message', handleMessage); + iframe.remove(); + backgroundUpdates.triggerUpdate(); + resolve(); + break; + + case 'MIGRATION_ERROR': + console.error('Migration failed:', event.data.error); + window.removeEventListener('message', handleMessage); + iframe.remove(); + reject(new Error(event.data.error)); + break; + } + }; + + window.addEventListener('message', handleMessage); + + const startPinging = () => { + const pingInterval = setInterval(() => { + iframe.contentWindow?.postMessage({ type: 'PING' }, '*'); + }, 500); + + const messageHandler = (event: MessageEvent) => { + if (event.source === iframe.contentWindow) { + clearInterval(pingInterval); + window.removeEventListener('message', messageHandler); + iframe.contentWindow?.postMessage({ type: 'START_MIGRATION' }, '*'); + } + }; + + window.addEventListener('message', messageHandler); + }; + + iframe.src = browser.runtime.getURL('seqta/utils/migration/migrate.html'); + document.body.appendChild(iframe); + startPinging(); + }); +}; + +const storeBackground = async (data: { + id: string; + blob: Blob; + type: 'image' | 'video'; +}): Promise => { + try { + await openDatabase(); + await writeData(data.id, data.type, data.blob); + } catch (error) { + console.error('Error storing background:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/seqta/utils/migration/migrate.html b/src/seqta/utils/migration/migrate.html new file mode 100644 index 00000000..94c36953 --- /dev/null +++ b/src/seqta/utils/migration/migrate.html @@ -0,0 +1,10 @@ + + + + + Background Migration + + + + + \ No newline at end of file diff --git a/src/seqta/utils/migration/migration-iframe.ts b/src/seqta/utils/migration/migration-iframe.ts new file mode 100644 index 00000000..bd42f4d9 --- /dev/null +++ b/src/seqta/utils/migration/migration-iframe.ts @@ -0,0 +1,109 @@ +// This goes in your migration.html's script +interface Data { + id: string; + blob: Blob; + type: 'image' | 'video'; +} + +const openDB = (): Promise => { + return new Promise((resolve, reject) => { + const request = indexedDB.open('MyDatabase', 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + }); +}; + +const blobToBase64 = (blob: Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const base64 = reader.result as string; + resolve(base64.split(',')[1]); // Remove data URL prefix + }; + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }); +}; + +const getAllBackgrounds = async (): Promise => { + const db = await openDB(); + const tx = db.transaction('backgrounds', 'readonly'); + const store = tx.objectStore('backgrounds'); + const request = store.getAll(); + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +}; + +const getSelectedBackground = (): string | null => { + return localStorage.getItem('selectedBackground'); +}; + +const startMigration = async () => { + try { + console.info('Starting background extraction...'); + const backgrounds = await getAllBackgrounds(); + const selectedBackground = getSelectedBackground(); + console.info(`Found ${backgrounds.length} backgrounds`); + + window.parent.postMessage({ type: 'GET_LAST_PROCESSED_ID' }, '*'); + + const lastProcessedId = await new Promise(resolve => { + const handler = (event: MessageEvent) => { + if (event.data.type === 'LAST_PROCESSED_ID') { + window.removeEventListener('message', handler); + resolve(event.data.id); + } + }; + window.addEventListener('message', handler); + }); + + const remainingBackgrounds = lastProcessedId + ? backgrounds.slice(backgrounds.findIndex(b => b.id === lastProcessedId) + 1) + : backgrounds; + + console.info(`Processing ${remainingBackgrounds.length} remaining backgrounds`); + + for (let i = 0; i < remainingBackgrounds.length; i++) { + const background = remainingBackgrounds[i]; + const base64Data = await blobToBase64(background.blob); + + window.parent.postMessage({ + type: 'BACKGROUND_DATA', + payload: { + id: background.id, + data: base64Data, + mediaType: background.type, + total: backgrounds.length, + processed: i + 1, + isSelected: background.id === selectedBackground + } + }, '*'); + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + window.parent.postMessage({ type: 'MIGRATION_COMPLETE' }, '*'); + + } catch (error: any) { + console.error('Extraction failed:', error); + window.parent.postMessage({ + type: 'MIGRATION_ERROR', + error: error.message || 'Unknown error' + }, '*'); + } +}; + +window.addEventListener('message', (event) => { + switch (event.data.type) { + case 'PING': + window.parent.postMessage({ type: 'PONG' }, '*'); + break; + + case 'START_MIGRATION': + startMigration(); + break; + } +}); \ No newline at end of file diff --git a/src/seqta/utils/sendThemeUpdate.ts b/src/seqta/utils/sendThemeUpdate.ts index 0fa6f4a4..8e895b8e 100644 --- a/src/seqta/utils/sendThemeUpdate.ts +++ b/src/seqta/utils/sendThemeUpdate.ts @@ -1,6 +1,6 @@ export default function sendThemeUpdate() { - const iframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement + /* const iframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement if (iframe) { iframe.contentWindow?.postMessage({ type: 'themeChanged' }, '*'); - } + } */ } \ No newline at end of file diff --git a/src/svelte.config.js b/src/svelte.config.js new file mode 100644 index 00000000..c099a835 --- /dev/null +++ b/src/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/src/interface/types/CustomThemes.ts b/src/types/CustomThemes.ts similarity index 63% rename from src/interface/types/CustomThemes.ts rename to src/types/CustomThemes.ts index 2191019d..79990f56 100644 --- a/src/interface/types/CustomThemes.ts +++ b/src/types/CustomThemes.ts @@ -7,7 +7,7 @@ export type CustomTheme = { allowBackgrounds: boolean; CustomCSS: string; CustomImages: CustomImage[]; - coverImage: Blob | string | null; + coverImage: Blob | null; isEditable: boolean; hideThemeName: boolean; webURL?: string; @@ -15,6 +15,16 @@ export type CustomTheme = { forceDark?: boolean; } +export type LoadedCustomTheme = CustomTheme & { + CustomImages: { + id: string; + blob: Blob; + variableName: string; + url: string | null; + }[]; + coverImageUrl?: string; +}; + export type DownloadedTheme = CustomTheme & { webURL: string; } @@ -25,18 +35,7 @@ export type CustomImage = { variableName: string; } -export type CustomImageBase64 = { - id: string; - url: string; - variableName: string; -} - -export type CustomThemeBase64 = Omit & { - CustomImages: CustomImageBase64[]; - coverImage: string | null; -} - export type ThemeList = { - themes: Omit[]; + themes: CustomTheme[]; selectedTheme: string; } \ No newline at end of file diff --git a/src/types/storage.ts b/src/types/storage.ts index 5d1fbfc5..2f79de5c 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -44,7 +44,7 @@ interface ToggleItem { toggle: boolean; } -interface Shortcut { +export interface Shortcut { enabled: boolean; name: string; } diff --git a/tailwind.config.js b/tailwind.config.js index 562f78d1..67df1fdf 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,8 +5,13 @@ const { /** @type {import('tailwindcss').Config} */ export default { content: [ - "./src/**/*.{js,ts,jsx,tsx,html}", + "./src/**/*.{js,ts,jsx,tsx,html,svelte}", ], + //safelist: [ + //{ + // pattern: / */, + //} + //], darkMode: "class", theme: { fontSize: { @@ -34,6 +39,9 @@ export default { fontFamily: { "IconFamily": "IconFamily" }, + animation: { + 'spin-fast': 'spin 0.4s linear infinite', + }, aspectRatio: { "theme": "5 / 1" } diff --git a/tsconfig.json b/tsconfig.json index 93693431..e5f763d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,5 @@ { + "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, @@ -8,10 +9,10 @@ /* Bundler mode */ "moduleResolution": "bundler", - "allowImportingTsExtensions": false, + "allowImportingTsExtensions": true, + "noEmit": true, "resolveJsonModule": true, "isolatedModules": true, - "noEmit": false, "jsx": "react-jsx", /* Linting */ @@ -22,6 +23,11 @@ "paths": { "@/*": ["./src/*"] - } + }, + "types": [ + "vite/client", + "node" + ] }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "src/interface/+layout.sveltes"] } diff --git a/vite.config.ts b/vite.config.ts index 535c56e4..1e8ead94 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,10 +5,12 @@ import { updateManifestPlugin } from './lib/patchPackage'; import { base64Loader } from './lib/base64loader'; import type { BuildTarget } from './lib/types'; -import react from '@vitejs/plugin-react-swc'; +import react from '@vitejs/plugin-react'; import million from "million/compiler"; //import MillionLint from '@million/lint'; +import { svelte } from '@sveltejs/vite-plugin-svelte' + import { chrome } from './src/manifests/chrome'; import { brave } from './src/manifests/brave'; import { edge } from './src/manifests/edge'; @@ -27,6 +29,9 @@ export default defineConfig({ plugins: [ base64Loader, react(), + svelte({ + emitCss: false + }), million.vite({ auto: true }), //MillionLint.vite(), /* enable for testing and debugging performance */ crx({ @@ -51,12 +56,12 @@ export default defineConfig({ }, build: { outDir: resolve(__dirname, 'dist', mode), - emptyOutDir: true, + emptyOutDir: false, minify: false, rollupOptions: { input: { settings: join(__dirname, 'src', 'interface', 'index.html'), - backgrounds: join(__dirname, 'src', 'seqta', 'ui', 'background', 'background.html') + migration: join(__dirname, 'src', 'seqta', 'utils', 'migration', 'migrate.html') } } }