├── .github └── workflows │ ├── beta-release.yml │ ├── create-release-assets │ └── action.yml │ ├── custom-changelog │ ├── action.yml │ └── dist │ │ ├── commit.hbs │ │ ├── footer.hbs │ │ ├── header.hbs │ │ ├── index.cjs │ │ ├── index.js.map │ │ ├── sourcemap-register.cjs │ │ └── template.hbs │ ├── nightly-release.yml │ ├── release.yml │ └── update-package-managers │ ├── action.yml │ ├── chocolatey │ └── action.yml │ └── winget │ └── action.yml ├── .gitignore ├── LICENSE ├── README.md ├── ThemeTypes.ts ├── backend ├── backendHelpers │ ├── changePreset.ts │ ├── deletePreset.ts │ └── index.ts ├── index.ts ├── pythonMethods │ ├── checkIfBackendIfStandalone.ts │ ├── deleteTheme.ts │ ├── downloadThemeFromUrl.ts │ ├── dummyFunction.ts │ ├── fetchThemePath.ts │ ├── generatePreset.ts │ ├── generatePresetFromThemeNames.ts │ ├── getBackendVersion.ts │ ├── getInstalledThemes.ts │ ├── getLastLoadErrors.ts │ ├── index.ts │ ├── reloadBackend.ts │ ├── server.ts │ ├── setComponentOfThemePatch.ts │ ├── setPatchOfTheme.ts │ ├── setThemeState.ts │ ├── storeRead.ts │ └── storeWrite.ts ├── recursiveCheck.ts ├── sleep.ts ├── tauriMethods │ ├── checkIfStandaloneBackendExists.ts │ ├── checkIfThemeExists.ts │ ├── copyBackend.ts │ ├── downloadBackend.ts │ ├── downloadTemplate.ts │ ├── getOS.ts │ ├── getStandaloneVersion.ts │ ├── index.ts │ ├── killBackend.ts │ ├── setStandaloneVersion.ts │ ├── startBackend.ts │ └── test.ts ├── toast.ts ├── webFetches │ ├── checkForNewBackend.ts │ ├── fetchNewestBackend.ts │ └── index.ts └── wrappedInvoke.ts ├── components ├── AppRoot.tsx ├── BackendFailedPage.tsx ├── CreatePresetModal.tsx ├── DownloadBackendPage.tsx ├── GenericInstallBackendModal.tsx ├── ManageThemes │ ├── ManageThemeCard.tsx │ ├── ThemeErrorsList.tsx │ ├── YourProfilesList.tsx │ └── index.ts ├── Native │ ├── AppFrame.tsx │ ├── DynamicTitlebar.tsx │ └── Titlebar.tsx ├── Nav │ ├── MainNav.tsx │ ├── NavTab.tsx │ └── index.ts ├── Presets │ ├── PresetSelectionDropdown.tsx │ ├── PresetThemeNameDisplayCard.tsx │ └── index.ts ├── Primitives │ ├── AlertDialog.tsx │ ├── Dropdown.tsx │ ├── InputAlertDialog.tsx │ ├── LabelledField.tsx │ ├── MenuDropdown.tsx │ ├── Modal.tsx │ ├── SimpleRadioDropdown.tsx │ ├── ToggleSwitch.tsx │ ├── Tooltip.tsx │ ├── TwoItemToggle.tsx │ └── index.ts ├── Settings │ ├── CreateTemplateTheme.tsx │ └── index.ts ├── SingleTheme │ ├── PatchComponent.tsx │ ├── ThemePatch.tsx │ ├── ThemeToggle.tsx │ └── index.ts ├── YourThemes │ ├── OneColumnThemeView.tsx │ ├── TwoColumnThemeView.tsx │ └── index.ts └── index.ts ├── constants.ts ├── contexts ├── FontContext.tsx ├── backendStatusContext.ts ├── osContext.ts └── themeContext.ts ├── hooks ├── index.ts ├── useBasicAsyncEffect.ts ├── useInterval.ts ├── usePlatform.ts └── useVW.ts ├── latest.json ├── logic ├── bulkThemeUpdateCheck.ts └── index.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── index.tsx ├── manage-themes.tsx ├── settings.tsx └── store.tsx ├── postcss.config.js ├── prettier.config.js ├── public ├── CSSLoaderWordmark.svg ├── favicon.ico ├── logo_css_darkmode.png └── logo_css_lightmode.png ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ └── main.rs └── tauri.conf.json ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.github/workflows/beta-release.yml: -------------------------------------------------------------------------------- 1 | name: Beta Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "dev" 7 | 8 | jobs: 9 | create-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-20.04 13 | outputs: 14 | release_id: ${{ steps.create-release.outputs.result }} 15 | 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup Node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | 25 | - name: Get Package Version 26 | run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 27 | 28 | - name: Create Release 29 | id: create-release 30 | uses: actions/github-script@v6 31 | with: 32 | script: | 33 | const { data } = await github.rest.repos.createRelease({ 34 | owner: context.repo.owner, 35 | repo: context.repo.repo, 36 | tag_name: `beta-${process.env.PACKAGE_VERSION}`, 37 | name: `CSSLoader Desktop Beta ${process.env.PACKAGE_VERSION}`, 38 | body: `Download the release for your platform below`, 39 | draft: true, 40 | prerelease: false 41 | }); 42 | 43 | core.setOutput("tag", `beta-${process.env.PACKAGE_VERSION}`); 44 | 45 | return data.id 46 | 47 | build-tauri: 48 | needs: create-release 49 | permissions: 50 | contents: write 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | platform: [windows-latest] 55 | 56 | runs-on: ${{ matrix.platform }} 57 | steps: 58 | - name: Checkout Repository 59 | uses: actions/checkout@v3 60 | with: 61 | fetch-depth: 0 62 | ref: "dev" 63 | 64 | - name: Setup Node 65 | uses: actions/setup-node@v3 66 | with: 67 | node-version: 16 68 | 69 | - name: Install Rust Stable 70 | uses: dtolnay/rust-toolchain@stable 71 | 72 | - name: Install Dependencies (Linux Only) 73 | if: matrix.platform == 'ubuntu-20.04' 74 | run: | 75 | sudo apt-get update 76 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 77 | 78 | - name: Install Frontend Dependencies 79 | run: npm install 80 | 81 | - name: Build App 82 | uses: tauri-apps/tauri-action@dev 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 86 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 87 | with: 88 | releaseId: ${{ needs.create-release.outputs.release_id }} 89 | tagName: ${{ needs.create-release.outputs.tag }} 90 | releaseBody: "Download the release for your platform below" 91 | releaseDraft: true 92 | includeUpdaterJson: false 93 | 94 | publish-release: 95 | needs: [create-release, build-tauri] 96 | permissions: 97 | contents: write 98 | runs-on: ubuntu-20.04 99 | 100 | steps: 101 | - name: Checkout Repository 102 | uses: actions/checkout@v3 103 | with: 104 | fetch-depth: 0 105 | ref: "dev" 106 | 107 | - name: Update Release Assets 108 | uses: ./.github/workflows/create-release-assets 109 | with: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | release_id: ${{ needs.create-release.outputs.release_id }} 112 | release_tag: ${{ needs.create-release.outputs.tag }} 113 | git_branch: "dev" 114 | 115 | - name: Publish Release 116 | id: publish-release 117 | uses: actions/github-script@v6 118 | env: 119 | release_id: ${{ needs.create-release.outputs.release_id }} 120 | with: 121 | script: | 122 | github.rest.repos.updateRelease({ 123 | owner: context.repo.owner, 124 | repo: context.repo.repo, 125 | release_id: process.env.release_id, 126 | draft: false, 127 | prerelease: false 128 | }) 129 | -------------------------------------------------------------------------------- /.github/workflows/create-release-assets/action.yml: -------------------------------------------------------------------------------- 1 | name: "create-release-assets" 2 | description: "Creates release assets." 3 | author: "Tormak" 4 | inputs: 5 | GITHUB_TOKEN: 6 | description: "Your GitHub token." 7 | required: true 8 | 9 | git_branch: 10 | description: "The target release branch." 11 | required: true 12 | 13 | release_id: 14 | description: "The target release id." 15 | required: true 16 | 17 | release_tag: 18 | description: "The target release tag." 19 | required: true 20 | 21 | runs: 22 | using: "composite" 23 | steps: 24 | - name: Update Release Assets 25 | uses: actions/github-script@v6 26 | env: 27 | GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} 28 | release_id: ${{ inputs.release_id }} 29 | release_tag: ${{ inputs.release_tag }} 30 | git_branch: ${{ inputs.git_branch }} 31 | with: 32 | script: | 33 | const fs = require("fs"); 34 | const path = require("path"); 35 | 36 | async function getReleaseAssetContents(id) { 37 | const contents = ( 38 | await github.request( 39 | 'GET /repos/{owner}/{repo}/releases/assets/{asset_id}', 40 | { 41 | owner: context.repo.owner, 42 | repo: context.repo.repo, 43 | asset_id: id, 44 | headers: { 45 | accept: 'application/octet-stream', 46 | }, 47 | } 48 | ) 49 | ).data; 50 | return contents; 51 | } 52 | 53 | async function deleteReleaseAsset(id) { 54 | await github.rest.repos.deleteReleaseAsset({ 55 | owner: context.repo.owner, 56 | repo: context.repo.repo, 57 | asset_id: id 58 | }); 59 | } 60 | 61 | async function uploadReleaseAsset(name, contents) { 62 | await github.rest.repos.uploadReleaseAsset({ 63 | owner: context.repo.owner, 64 | repo: context.repo.repo, 65 | release_id: process.env.release_id, 66 | name: name, 67 | data: contents 68 | }); 69 | } 70 | 71 | async function setReleaseAssetName(id, newName) { 72 | await github.rest.repos.updateReleaseAsset({ 73 | owner: context.repo.owner, 74 | repo: context.repo.repo, 75 | asset_id: id, 76 | name: newName 77 | }); 78 | } 79 | 80 | core.info(`Tag: ${process.env.release_tag}`) 81 | core.info(`Branch: ${process.env.git_branch}`) 82 | 83 | let versionNoV = process.env.release_tag.substring(1); 84 | if (process.env.git_branch === "nightly") { 85 | versionNoV = process.env.release_tag 86 | } 87 | const GENERIC_NAMES = { 88 | "windowsInstaller": `CSSLoader.Desktop_${versionNoV}.msi`, 89 | "windowsUpdater": `CSSLoader.Desktop_${versionNoV}.msi.zip`, 90 | "windowsUpdaterSig": `CSSLoader.Desktop_${versionNoV}.msi.zip.sig`, 91 | "linuxInstaller": `CSSLoader.Desktop_${versionNoV}.AppImage`, 92 | "linuxUpdater": `CSSLoader.Desktop_${versionNoV}.AppImage.tar.gz`, 93 | "linuxUpdaterSig": `CSSLoader.Desktop_${versionNoV}.AppImage.tar.gz.sig`, 94 | } 95 | 96 | const assets = await github.rest.repos.listReleaseAssets({ 97 | owner: context.repo.owner, 98 | repo: context.repo.repo, 99 | release_id: process.env.release_id 100 | }); 101 | 102 | 103 | const winInstaller = assets.data.find((asset) => asset.name.endsWith(".msi")); 104 | await setReleaseAssetName(winInstaller.id, GENERIC_NAMES["windowsInstaller"]); 105 | 106 | const winUpdater = assets.data.find((asset) => asset.name.endsWith(".msi.zip")); 107 | await setReleaseAssetName(winUpdater.id, GENERIC_NAMES["windowsUpdater"]); 108 | 109 | const winUpdaterSig = assets.data.find((asset) => asset.name.endsWith(".msi.zip.sig")); 110 | await setReleaseAssetName(winUpdaterSig.id, GENERIC_NAMES["windowsUpdaterSig"]); 111 | 112 | const linuxInstaller = assets.data.find((asset) => asset.name.endsWith(".AppImage")); 113 | await setReleaseAssetName(linuxInstaller.id, GENERIC_NAMES["linuxInstaller"]); 114 | 115 | const linuxUpdater = assets.data.find((asset) => asset.name.endsWith(".AppImage.tar.gz")); 116 | await setReleaseAssetName(linuxUpdater.id, GENERIC_NAMES["linuxUpdater"]); 117 | 118 | const linuxUpdaterSig = assets.data.find((asset) => asset.name.endsWith(".AppImage.tar.gz.sig")); 119 | await setReleaseAssetName(linuxUpdaterSig.id, GENERIC_NAMES["linuxUpdaterSig"]); 120 | 121 | const latest = assets.data.find((asset) => asset.name === "latest.json"); 122 | const latestContentsBuff = Buffer.from(await getReleaseAssetContents(latest.id)); 123 | let latestContents = latestContentsBuff.toString(); 124 | await deleteReleaseAsset(latest.id); 125 | 126 | latestContents = latestContents.replace(winUpdater.name, GENERIC_NAMES["windowsUpdater"]); 127 | latestContents = latestContents.replace(linuxUpdater.name, GENERIC_NAMES["linuxUpdater"]); 128 | 129 | await uploadReleaseAsset("latest.json", Buffer.from(latestContents)); 130 | 131 | const latestPath = path.resolve(process.cwd(), "latest.json"); 132 | fs.writeFileSync(latestPath, Buffer.from(latestContents)); 133 | 134 | const config = (prop, value) => exec.exec(`git config ${prop} "${value}"`); 135 | const add = (file) => exec.exec(`git add ${file}`); 136 | const commit = (message) => exec.exec(`git commit -m "${message}"`); 137 | const push = (branch) => exec.exec(`git push origin ${branch} --follow-tags`); 138 | const updateOrigin = (repo) => exec.exec(`git remote set-url origin ${repo}`); 139 | 140 | core.setSecret(process.env.GITHUB_TOKEN); 141 | 142 | updateOrigin(`https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/${process.env.GITHUB_REPOSITORY}.git`); 143 | config('user.email', "cssloaderdesktop.release.action@github.com"); 144 | config('user.name', "CSSLoader Desktop Release Action"); 145 | 146 | await add("."); 147 | await commit("chore(release): updating latest.json to generated version."); 148 | await push(process.env.git_branch); 149 | 150 | core.info("Committed changes to latest.json complete!"); 151 | -------------------------------------------------------------------------------- /.github/workflows/custom-changelog/action.yml: -------------------------------------------------------------------------------- 1 | name: 'custom-changelog-action' 2 | description: 'Generates changelogs for commitlint commits and is compatible with tauri.' 3 | author: 'Tormak' 4 | inputs: 5 | github-token: 6 | description: "Github token" 7 | default: ${{ github.token }} 8 | required: false 9 | 10 | git-message: 11 | description: "Commit message to use" 12 | default: "chore(release): {version}" 13 | required: false 14 | 15 | git-user-name: 16 | description: "The git user.name to use for the commit" 17 | default: "Conventional Changelog Action" 18 | required: false 19 | 20 | git-user-email: 21 | description: "The git user.email to use for the commit" 22 | default: "conventional.changelog.action@github.com" 23 | required: false 24 | 25 | git-pull-method: 26 | description: "The git pull method used when pulling all changes from remote" 27 | default: "--ff-only" 28 | required: false 29 | 30 | git-branch: 31 | description: "The git branch to be pushed" 32 | default: ${{ github.ref }} 33 | required: false 34 | 35 | tag-prefix: 36 | description: "Prefix that is used for the git tag" 37 | default: "v" 38 | required: false 39 | 40 | git-url: 41 | description: "Git Url" 42 | default: "github.com" 43 | required: false 44 | 45 | outputs: 46 | clean_changelog: 47 | description: "A tidied version of the generated changelog." 48 | 49 | tag: 50 | description: "The tag for the new release." 51 | 52 | version: 53 | description: "The version for the new release." 54 | 55 | runs: 56 | using: 'node16' 57 | main: 'dist/index.cjs' 58 | -------------------------------------------------------------------------------- /.github/workflows/custom-changelog/dist/commit.hbs: -------------------------------------------------------------------------------- 1 | * {{header}} 2 | 3 | {{~!-- commit link --}} {{#if @root.linkReferences~}} 4 | ([{{hash}}]( 5 | {{~#if @root.repository}} 6 | {{~#if @root.host}} 7 | {{~@root.host}}/ 8 | {{~/if}} 9 | {{~#if @root.owner}} 10 | {{~@root.owner}}/ 11 | {{~/if}} 12 | {{~@root.repository}} 13 | {{~else}} 14 | {{~@root.repoUrl}} 15 | {{~/if}}/ 16 | {{~@root.commit}}/{{hash}})) 17 | {{~else}} 18 | {{~hash}} 19 | {{~/if}} 20 | 21 | {{~!-- commit references --}} 22 | {{~#if references~}} 23 | , closes 24 | {{~#each references}} {{#if @root.linkReferences~}} 25 | [ 26 | {{~#if this.owner}} 27 | {{~this.owner}}/ 28 | {{~/if}} 29 | {{~this.repository}}#{{this.issue}}]( 30 | {{~#if @root.repository}} 31 | {{~#if @root.host}} 32 | {{~@root.host}}/ 33 | {{~/if}} 34 | {{~#if this.repository}} 35 | {{~#if this.owner}} 36 | {{~this.owner}}/ 37 | {{~/if}} 38 | {{~this.repository}} 39 | {{~else}} 40 | {{~#if @root.owner}} 41 | {{~@root.owner}}/ 42 | {{~/if}} 43 | {{~@root.repository}} 44 | {{~/if}} 45 | {{~else}} 46 | {{~@root.repoUrl}} 47 | {{~/if}}/ 48 | {{~@root.issue}}/{{this.issue}}) 49 | {{~else}} 50 | {{~#if this.owner}} 51 | {{~this.owner}}/ 52 | {{~/if}} 53 | {{~this.repository}}#{{this.issue}} 54 | {{~/if}}{{/each}} 55 | {{~/if}} 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/custom-changelog/dist/footer.hbs: -------------------------------------------------------------------------------- 1 | {{#if noteGroups}} 2 | {{#each noteGroups}} 3 | 4 | ### {{title}} 5 | 6 | {{#each notes}} 7 | * {{text}} 8 | {{/each}} 9 | {{/each}} 10 | {{/if}} 11 | -------------------------------------------------------------------------------- /.github/workflows/custom-changelog/dist/header.hbs: -------------------------------------------------------------------------------- 1 | ## {{#if isPatch~}} 2 | {{~/if~}} {{version}} 3 | {{~#if title}} "{{title}}" 4 | {{~/if~}} 5 | {{~#if date}} ({{date}}) 6 | {{~/if~}} 7 | {{~#if isPatch~}} 8 | {{~/if}} 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/custom-changelog/dist/template.hbs: -------------------------------------------------------------------------------- 1 | {{> header}} 2 | 3 | {{#each commitGroups}} 4 | {{#each commits}} 5 | {{> commit root=@root}} 6 | {{/each}} 7 | {{/each}} 8 | 9 | {{> footer}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/nightly-release.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "nightly" 7 | 8 | jobs: 9 | create-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-20.04 13 | outputs: 14 | release_id: ${{ steps.create-release.outputs.result }} 15 | change_log: ${{ steps.changelog.outputs.clean_changelog }} 16 | version: ${{ steps.changelog.outputs.version }} 17 | tag: ${{ steps.changelog.outputs.tag }} 18 | 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | 28 | - name: Get Commit Hash 29 | run: echo "COMMIT_HASH=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV 30 | 31 | - name: Get Package Version 32 | run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 33 | 34 | - name: Create Release 35 | id: create-release 36 | uses: actions/github-script@v6 37 | with: 38 | script: | 39 | const { data } = await github.rest.repos.createRelease({ 40 | owner: context.repo.owner, 41 | repo: context.repo.repo, 42 | tag_name: `nightly-${process.env.COMMIT_HASH}`, 43 | name: `CSSLoader Desktop Nightly ${process.env.COMMIT_HASH}`, 44 | body: `Download the release for your platform below`, 45 | draft: true, 46 | prerelease: true 47 | }); 48 | 49 | core.setOutput("tag", `nightly-${process.env.COMMIT_HASH}`); 50 | 51 | return data.id 52 | 53 | build-tauri: 54 | needs: create-release 55 | permissions: 56 | contents: write 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | platform: [ubuntu-20.04, windows-latest] 61 | 62 | runs-on: ${{ matrix.platform }} 63 | steps: 64 | - name: Checkout Repository 65 | uses: actions/checkout@v3 66 | with: 67 | fetch-depth: 0 68 | ref: "nightly" 69 | 70 | - name: Setup Node 71 | uses: actions/setup-node@v3 72 | with: 73 | node-version: 16 74 | 75 | - name: Install Rust Stable 76 | uses: dtolnay/rust-toolchain@stable 77 | 78 | - name: Install Dependencies (Linux Only) 79 | if: matrix.platform == 'ubuntu-20.04' 80 | run: | 81 | sudo apt-get update 82 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 83 | 84 | - name: Install Frontend Dependencies 85 | run: npm install 86 | 87 | - name: Build App 88 | uses: tauri-apps/tauri-action@dev 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 92 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 93 | with: 94 | releaseId: ${{ needs.create-release.outputs.release_id }} 95 | tagName: ${{ needs.create-release.outputs.tag }} 96 | releaseBody: "Download the release for your platform below" 97 | releaseDraft: true 98 | includeUpdaterJson: true 99 | 100 | publish-release: 101 | needs: [create-release, build-tauri] 102 | permissions: 103 | contents: write 104 | runs-on: ubuntu-20.04 105 | 106 | steps: 107 | - name: Checkout Repository 108 | uses: actions/checkout@v3 109 | with: 110 | fetch-depth: 0 111 | ref: "nightly" 112 | 113 | - name: Update Release Assets 114 | uses: ./.github/workflows/create-release-assets 115 | with: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | release_id: ${{ needs.create-release.outputs.release_id }} 118 | release_tag: ${{ needs.create-release.outputs.tag }} 119 | git_branch: "nightly" 120 | 121 | - name: Publish Release 122 | id: publish-release 123 | uses: actions/github-script@v6 124 | env: 125 | release_id: ${{ needs.create-release.outputs.release_id }} 126 | with: 127 | script: | 128 | github.rest.repos.updateRelease({ 129 | owner: context.repo.owner, 130 | repo: context.repo.repo, 131 | release_id: process.env.release_id, 132 | draft: false, 133 | prerelease: true 134 | }) 135 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | create-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-20.04 13 | outputs: 14 | release_id: ${{ steps.create-release.outputs.result }} 15 | change_log: ${{ steps.changelog.outputs.clean_changelog }} 16 | version: ${{ steps.changelog.outputs.version }} 17 | tag: ${{ steps.changelog.outputs.tag }} 18 | 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | 28 | - name: Conventional Changelog Action 29 | id: changelog 30 | uses: ./.github/workflows/custom-changelog 31 | with: 32 | github-token: ${{ secrets.github_token }} 33 | 34 | - name: Create Release 35 | id: create-release 36 | uses: actions/github-script@v6 37 | env: 38 | RELEASE_TAG: ${{ steps.changelog.outputs.tag }} 39 | RELEASE_LOG: ${{ steps.changelog.outputs.clean_changelog }} 40 | with: 41 | script: | 42 | const { data } = await github.rest.repos.createRelease({ 43 | owner: context.repo.owner, 44 | repo: context.repo.repo, 45 | tag_name: `${process.env.RELEASE_TAG}`, 46 | name: `CSSLoader Desktop ${process.env.RELEASE_TAG}`, 47 | body: `${process.env.RELEASE_LOG}`, 48 | draft: true, 49 | prerelease: false 50 | }); 51 | return data.id 52 | 53 | build-tauri: 54 | needs: create-release 55 | permissions: 56 | contents: write 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | platform: [ubuntu-20.04, windows-latest] 61 | 62 | runs-on: ${{ matrix.platform }} 63 | steps: 64 | - name: Checkout Repository 65 | uses: actions/checkout@v3 66 | with: 67 | fetch-depth: 0 68 | ref: "main" 69 | 70 | - name: Setup Node 71 | uses: actions/setup-node@v3 72 | with: 73 | node-version: 16 74 | 75 | - name: Install Rust Stable 76 | uses: dtolnay/rust-toolchain@stable 77 | 78 | - name: Install Dependencies (Linux Only) 79 | if: matrix.platform == 'ubuntu-20.04' 80 | run: | 81 | sudo apt-get update 82 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 83 | 84 | - name: Install Frontend Dependencies 85 | run: npm install 86 | 87 | - name: Build App 88 | uses: tauri-apps/tauri-action@dev 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 92 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 93 | with: 94 | releaseId: ${{ needs.create-release.outputs.release_id }} 95 | tagName: ${{ needs.create-release.outputs.tag }} 96 | releaseBody: "Check Github for the release notes!" 97 | releaseDraft: true 98 | includeUpdaterJson: true 99 | 100 | publish-release: 101 | needs: [create-release, build-tauri] 102 | permissions: 103 | contents: write 104 | runs-on: ubuntu-20.04 105 | 106 | steps: 107 | - name: Checkout Repository 108 | uses: actions/checkout@v3 109 | with: 110 | fetch-depth: 0 111 | ref: "main" 112 | 113 | - name: Update Release Assets 114 | uses: ./.github/workflows/create-release-assets 115 | with: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | release_id: ${{ needs.create-release.outputs.release_id }} 118 | release_tag: ${{ needs.create-release.outputs.tag }} 119 | git_branch: "main" 120 | 121 | - name: Publish Release 122 | id: publish-release 123 | uses: actions/github-script@v6 124 | env: 125 | release_id: ${{ needs.create-release.outputs.release_id }} 126 | with: 127 | script: | 128 | github.rest.repos.updateRelease({ 129 | owner: context.repo.owner, 130 | repo: context.repo.repo, 131 | release_id: process.env.release_id, 132 | draft: false, 133 | prerelease: false 134 | }) 135 | -------------------------------------------------------------------------------- /.github/workflows/update-package-managers/action.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/.github/workflows/update-package-managers/action.yml -------------------------------------------------------------------------------- /.github/workflows/update-package-managers/chocolatey/action.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/.github/workflows/update-package-managers/chocolatey/action.yml -------------------------------------------------------------------------------- /.github/workflows/update-package-managers/winget/action.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/.github/workflows/update-package-managers/winget/action.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

CSSLoader Desktop

5 |

A native standalone styling engine for Steam on Windows and Linux

6 |

Inject custom styles into Steam's desktop and Big Picture Mode interfaces, browse an ever-growing storefront of community creations, easily manage your themes' settings and updates, and more.

7 |

8 | 9 |

10 | 11 | # Features 12 | 13 | ## Theming 14 | 15 | - Injects user generated CSS into any render target of Steam UI. 16 | - Mix and match multiple themes to your liking. 17 | - Supports "patches" for themes, allowing users to customize values using dropdowns, color pickers, and more. 18 | - Allows custom images and fonts to be used. (See [Creating Symlink](#creating-symlink)) 19 | - "Profiles" allow you to save the state of multiple themes to reapply later. 20 | 21 | ## Theme Store 22 | 23 | - An integrated storefront for you to download user-submitted themes. 24 | - Prompts for updating your themes available in the 'Manage' tab. 25 | - Account support to enable the starring of themes. 26 | 27 | # Installation 28 | 29 | ## Downloading CSSLoader Desktop 30 | 31 | ### Windows 32 | 33 | 1. Download the latest CSSLoader Desktop `.msi` installer file from [Releases](https://github.com/beebls/CSSLoader-Desktop/releases/latest/). 34 | 2. Once downloaded, run the installer, and follow the onscreen instructions. 35 | 3. If this is your first time installing CSSLoader Desktop, upon launching the app you will be prompted to install CSSLoader's Backend. Click 'Install' 36 | 4. CSSLoader Desktop is now installed! Continue to [Creating Symlink](#creating-symlink) if you want to set up custom images/fonts. 37 | 38 | ### Linux/Steam Deck 39 | 40 | #### Installing CSSLoader's Backend 41 | 42 | 1. [Install Decky Loader using the instructions on the Decky repository.](https://github.com/SteamDeckHomebrew/decky-loader#-installation) 43 | 2. Run Steam's Big Picture mode or open game mode on Steam Deck. 44 | 3. Open the Quick Access Menu. (QAM Button, Ctrl + 2, Xbox + A, or PS + X) 45 | 4. Select the Decky tab, it has the icon of a power plug. 46 | 5. Select the store icon in the top right. 47 | 6. Search for 'CSS Loader' and install it. 48 | 7. Leave the store, and select CSS Loader from the plugin menu. 49 | 8. Select 'Download Themes', then go to the 'Settings' tab in the Theme Store. 50 | 9. Enable the toggle for "Enable Standalone Backend" 51 | 52 | #### Installing CSSLoader Desktop 53 | 54 | 1. Download the latest `.appimage` binary from [Releases](https://github.com/beebls/CSSLoader-Desktop/releases/latest/). 55 | 2. Running it will start CSSLoader Desktop. 56 | 3. Since CSSLoader Desktop cannot update the backend from the frontend, you will need to periodically open Steam Big Picture and update CSSLoader through Decky. 57 | 58 | ## Creating Symlink 59 | 60 | In order to have custom images, fonts, and other files work in themes, you need to create a symlink. 61 | 62 | If you are on Linux, you may skip this step as the symlink is created automatically. 63 | 64 | ### Windows 65 | 66 | In order to create the symlink, you must run CSSLoader's Backend at least once with Windows Developer Mode enabled. 67 | 68 | 1. Open Settings (Win + I) 69 | 2. Select 'Privacy & Security' 70 | - On Windows 10, this is called 'Updates and Security' 71 | 3. Select 'For developers' 72 | 4. Toggle 'Developer Mode' on. 73 | 5. Restart CSSLoader's Backend. 74 | - You can do this by opening CSSLoader Desktop, clicking on the 'Settings' tab, and pressing 'Kill Backend', followed by 'Force Start Backend' 75 | - Alternatively, you can restart your system. 76 | 6. Click on your system tray, and right click on the white paint roller icon. 77 | - If you see the text "Custom Images/Fonts Enabled", you're set! 78 | - You may disable Developer Mode after going through this process. 79 | 80 | # Building/Contribution 81 | 82 | ## Frontend 83 | 84 | As a Tauri app, you will need all of [Tauri's prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites) to develop for CSSLoader Desktop. 85 | 86 | To start a dev server, clone this repository, then run 87 | 88 | ``` 89 | npm i 90 | npm run tauri dev 91 | ``` 92 | 93 | A maintained list of frontend bugs/upcoming features is available through our [project board](https://fyro.notion.site/31266833d05746b19d63a72c4a69b649), we refer to issues based on their IDs in the board. 94 | 95 | ## Store 96 | 97 | The DeckThemes store is managed under [a different github repository](https://github.com/beebls/DeckThemes). 98 | 99 | ## Backend 100 | 101 | CSSLoader's backend is managed under [a different github repository](https://github.com/suchmememanyskill/SDH-CSSLoader). 102 | 103 | # Acknowledgements 104 | -------------------------------------------------------------------------------- /ThemeTypes.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react"; 2 | 3 | export interface Theme { 4 | id: string; 5 | enabled: boolean; // used to be called checked 6 | name: string; 7 | display_name?: string; 8 | author: string; 9 | bundled: boolean; // deprecated 10 | require: number; 11 | version: string; 12 | patches: Patch[]; 13 | dependencies: string[]; 14 | flags: Flags[]; 15 | created: number; 16 | modified: number; 17 | } 18 | 19 | export interface ThemePatchComponent { 20 | name: string; 21 | on: string; 22 | type: string; 23 | value: string; 24 | } 25 | 26 | export interface Patch { 27 | default: string; 28 | name: string; 29 | type: "dropdown" | "checkbox" | "slider" | "none"; 30 | value: string; 31 | options: string[]; 32 | components: ThemePatchComponent[]; 33 | } 34 | 35 | export enum Flags { 36 | "isPreset" = "PRESET", 37 | "dontDisableDeps" = "KEEP_DEPENDENCIES", 38 | "optionalDeps" = "OPTIONAL_DEPENDENCIES", 39 | } 40 | 41 | // API TYPES 42 | 43 | export interface UserInfo { 44 | id: string; 45 | username: string; 46 | avatar: string; 47 | } 48 | export interface MinimalCSSThemeInfo { 49 | id: string; 50 | name: string; 51 | version: string; 52 | target: string; 53 | manifestVersion: number; 54 | specifiedAuthor: string; 55 | type: "Css" | "Audio"; 56 | } 57 | 58 | export type BlobType = "Zip" | "Jpg"; 59 | 60 | export interface APIBlob { 61 | id: string; 62 | blobType: BlobType; 63 | uploaded: Date; 64 | downloadCount: number; 65 | } 66 | 67 | export interface PartialCSSThemeInfo extends MinimalCSSThemeInfo { 68 | images: APIBlob[]; 69 | download: APIBlob; 70 | author: UserInfo; 71 | submitted: Date; 72 | updated: Date; 73 | starCount: number; 74 | } 75 | 76 | export interface FullCSSThemeInfo extends PartialCSSThemeInfo { 77 | dependencies: MinimalCSSThemeInfo[]; 78 | approved: boolean; 79 | disabled: boolean; 80 | description: string; 81 | source?: string; 82 | } 83 | 84 | export enum Permissions { 85 | "editAny" = "EditAnyPost", 86 | "approveSubs" = "ApproveThemeSubmissions", 87 | "viewSubs" = "ViewThemeSubmissions", 88 | "admin" = "ManageApi", 89 | } 90 | 91 | export interface AccountData extends UserInfo { 92 | permissions: Permissions[]; 93 | } 94 | 95 | export interface StarredThemeList { 96 | total: number; 97 | items: PartialCSSThemeInfo[]; 98 | } 99 | 100 | type ThemeErrorTitle = string; 101 | type ThemeErrorDescription = string; 102 | export type ThemeError = [ThemeErrorTitle, ThemeErrorDescription]; 103 | -------------------------------------------------------------------------------- /backend/backendHelpers/changePreset.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "ThemeTypes"; 2 | import { setThemeState } from "backend/pythonMethods"; 3 | 4 | export async function changePreset(themeName: string, themeList: Theme[]) { 5 | return new Promise(async (resolve) => { 6 | // Disables all themes before enabling the preset 7 | await Promise.all(themeList.filter((e) => e.enabled).map((e) => setThemeState(e.name, false))); 8 | 9 | themeName !== "None" && (await setThemeState(themeName, true)); 10 | resolve(true); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /backend/backendHelpers/deletePreset.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "ThemeTypes"; 2 | import { changePreset } from "./changePreset"; 3 | import { deleteTheme } from "backend/pythonMethods"; 4 | 5 | export async function deletePreset( 6 | presetName: string, 7 | themes: Theme[], 8 | refreshThemes: (e?: boolean) => void 9 | ) { 10 | if (themes.find((e) => e.name === presetName)!.enabled) { 11 | await changePreset("Default Profile", themes); 12 | } 13 | await deleteTheme(presetName); 14 | refreshThemes(); 15 | } 16 | -------------------------------------------------------------------------------- /backend/backendHelpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./changePreset"; 2 | export * from "./deletePreset"; 3 | -------------------------------------------------------------------------------- /backend/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pythonMethods"; 2 | export * from "./tauriMethods"; 3 | export * from "./webFetches"; 4 | export * from "./toast"; 5 | export * from "./recursiveCheck"; 6 | export * from "./wrappedInvoke"; 7 | export * from "./sleep"; 8 | export * from "./backendHelpers"; 9 | -------------------------------------------------------------------------------- /backend/pythonMethods/checkIfBackendIfStandalone.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export async function checkIfBackendIsStandalone() { 3 | return server! 4 | .callPluginMethod<{}, boolean>("is_standalone", {}) 5 | .then((data) => { 6 | if (data.success) { 7 | return data.result; 8 | } 9 | return true; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /backend/pythonMethods/deleteTheme.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function deleteTheme(themeName: string): Promise { 3 | return server!.callPluginMethod("delete_theme", { themeName: themeName }); 4 | } 5 | -------------------------------------------------------------------------------- /backend/pythonMethods/downloadThemeFromUrl.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function downloadThemeFromUrl(themeId: string): Promise { 3 | return server!.callPluginMethod("download_theme_from_url", { 4 | id: themeId, 5 | url: "https://api.deckthemes.com/", 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /backend/pythonMethods/dummyFunction.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export async function dummyFunction() { 3 | try { 4 | return await server!.callPluginMethod<{}, boolean>("dummy_function", {}); 5 | } catch { 6 | // If backend is not running, fetch fails 7 | return false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/pythonMethods/fetchThemePath.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function fetchThemePath() { 3 | return server!.callPluginMethod<{}, string>("fetch_theme_path", {}); 4 | } 5 | -------------------------------------------------------------------------------- /backend/pythonMethods/generatePreset.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function generatePreset(name: string) { 3 | return server!.callPluginMethod("generate_preset_theme", { name: name }); 4 | } 5 | -------------------------------------------------------------------------------- /backend/pythonMethods/generatePresetFromThemeNames.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function generatePresetFromThemeNames(name: string, themeNames: string[]) { 3 | return server!.callPluginMethod("generate_preset_theme_from_theme_names", { 4 | name: name, 5 | themeNames: themeNames, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /backend/pythonMethods/getBackendVersion.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function getBackendVersion(): Promise { 3 | return server!.callPluginMethod("get_backend_version", {}); 4 | } 5 | -------------------------------------------------------------------------------- /backend/pythonMethods/getInstalledThemes.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../ThemeTypes"; 2 | import { server } from "./server"; 3 | export function getInstalledThemes(): Promise { 4 | return server! 5 | .callPluginMethod<{}, Theme[]>("get_themes", {}) 6 | .then((data) => { 7 | if (data.success) { 8 | return data.result; 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /backend/pythonMethods/getLastLoadErrors.ts: -------------------------------------------------------------------------------- 1 | import { ThemeError } from "../../ThemeTypes"; 2 | import { server } from "./server"; 3 | export function getLastLoadErrors(): Promise { 4 | return server! 5 | .callPluginMethod<{}, { fails: ThemeError[] }>("get_last_load_errors", {}) 6 | .then((data) => { 7 | if (data.success) { 8 | return data.result.fails; 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /backend/pythonMethods/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./checkIfBackendIfStandalone"; 2 | export * from "./server"; 3 | export * from "./dummyFunction"; 4 | export * from "./getInstalledThemes"; 5 | export * from "./reloadBackend"; 6 | export * from "./setThemeState"; 7 | export * from "./setPatchOfTheme"; 8 | export * from "./setComponentOfThemePatch"; 9 | export * from "./downloadThemeFromUrl"; 10 | export * from "./storeRead"; 11 | export * from "./storeWrite"; 12 | export * from "./deleteTheme"; 13 | export * from "./getBackendVersion"; 14 | export * from "./generatePreset"; 15 | export * from "./fetchThemePath"; 16 | export * from "./generatePresetFromThemeNames"; 17 | export * from "./getLastLoadErrors"; 18 | -------------------------------------------------------------------------------- /backend/pythonMethods/reloadBackend.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../ThemeTypes"; 2 | import { getInstalledThemes } from "./getInstalledThemes"; 3 | import { server } from "./server"; 4 | export function reloadBackend(): Promise { 5 | return server!.callPluginMethod("reset", {}).then(() => { 6 | return getInstalledThemes(); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /backend/pythonMethods/server.ts: -------------------------------------------------------------------------------- 1 | import { Body, fetch } from "@tauri-apps/api/http"; 2 | export interface Server { 3 | callPluginMethod( 4 | methodName: string, 5 | args: TArgs 6 | ): Promise>; 7 | } 8 | export type ServerResponse = ServerResponseSuccess | ServerResponseError; 9 | interface ServerResponseSuccess { 10 | success: true; 11 | result: TRes; 12 | } 13 | interface ServerResponseError { 14 | success: false; 15 | result: string; 16 | } 17 | export const server: Server = { 18 | async callPluginMethod(methodName: string, args: any) { 19 | return fetch("http://127.0.0.1:35821/req", { 20 | method: "POST", 21 | body: Body.json({ 22 | method: methodName, 23 | args: args, 24 | }), 25 | }) 26 | .then((res) => { 27 | return res.data; 28 | }) 29 | .then((json: any) => { 30 | return { 31 | result: json?.res || undefined, 32 | success: json?.success || false, 33 | }; 34 | }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /backend/pythonMethods/setComponentOfThemePatch.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function setComponentOfThemePatch( 3 | themeName: string, 4 | patchName: string, 5 | componentName: string, 6 | value: string 7 | ): Promise { 8 | return server!.callPluginMethod("set_component_of_theme_patch", { 9 | themeName: themeName, 10 | patchName: patchName, 11 | componentName: componentName, 12 | value: value, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /backend/pythonMethods/setPatchOfTheme.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function setPatchOfTheme( 3 | themeName: string, 4 | patchName: string, 5 | value: string 6 | ): Promise { 7 | return server!.callPluginMethod("set_patch_of_theme", { 8 | themeName: themeName, 9 | patchName: patchName, 10 | value: value, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /backend/pythonMethods/setThemeState.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function setThemeState( 3 | name: string, 4 | state: boolean, 5 | set_deps?: boolean, 6 | set_deps_value?: boolean 7 | ): Promise { 8 | return server!.callPluginMethod("set_theme_state", { 9 | name: name, 10 | state: state, 11 | set_deps: set_deps ?? true, 12 | set_deps_value: set_deps_value ?? true, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /backend/pythonMethods/storeRead.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function storeRead(key: string) { 3 | return server!.callPluginMethod<{ key: string }, string>("store_read", { 4 | key: key, 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /backend/pythonMethods/storeWrite.ts: -------------------------------------------------------------------------------- 1 | import { server } from "./server"; 2 | export function storeWrite(key: string, value: string) { 3 | return server!.callPluginMethod<{ key: string; val: string }, void>( 4 | "store_write", 5 | { key: key, val: value } 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /backend/recursiveCheck.ts: -------------------------------------------------------------------------------- 1 | export async function recursiveCheck( 2 | testFunc: any, 3 | onTrue: any, 4 | onFirstFalse: any 5 | ) { 6 | const recursive = async () => { 7 | const value = await testFunc(); 8 | if (value) { 9 | onTrue(); 10 | return; 11 | } else 12 | setTimeout(() => { 13 | recursive(); 14 | }, 1000); 15 | }; 16 | 17 | const value = await testFunc(); 18 | if (value) { 19 | onTrue(); 20 | return; 21 | } else { 22 | onFirstFalse(); 23 | recursive(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(duration: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, duration); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /backend/tauriMethods/checkIfStandaloneBackendExists.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseDirectory, 3 | exists, 4 | } from "@tauri-apps/api/fs"; 5 | export async function checkIfStandaloneBackendExists() { 6 | const backendExists = await exists( 7 | "Microsoft\\Windows\\Start Menu\\Programs\\Startup\\CssLoader-Standalone-Headless.exe", 8 | { 9 | dir: BaseDirectory.Config, 10 | } 11 | ); 12 | return backendExists; 13 | } -------------------------------------------------------------------------------- /backend/tauriMethods/checkIfThemeExists.ts: -------------------------------------------------------------------------------- 1 | import { exists, BaseDirectory } from "@tauri-apps/api/fs"; 2 | 3 | export async function checkIfThemeExists(themeName: string): Promise { 4 | return await exists(`homebrew/themes/${themeName}`, { 5 | dir: BaseDirectory.Home, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /backend/tauriMethods/copyBackend.ts: -------------------------------------------------------------------------------- 1 | import { wrappedInvoke } from "backend/wrappedInvoke"; 2 | export async function copyBackend(backendPath: string) { 3 | return await wrappedInvoke("copyBackend", [ 4 | "Copy-Item", 5 | "-Path", 6 | backendPath, 7 | "-Destination", 8 | "([Environment]::GetFolderPath('Startup') + '\\CssLoader-Standalone-Headless.exe')", 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /backend/tauriMethods/downloadBackend.ts: -------------------------------------------------------------------------------- 1 | import { fetchNewestBackend } from "backend/webFetches"; 2 | import semver from "semver"; 3 | import { setStandaloneVersion } from "./setStandaloneVersion"; 4 | 5 | export async function downloadBackend() { 6 | // Not sure if this is needed 7 | const { invoke } = await import("@tauri-apps/api"); 8 | 9 | const release = await fetchNewestBackend(); 10 | const url = release?.assets?.find((e: any) => 11 | e.name.includes("Standalone-Headless.exe") 12 | ).browser_download_url; 13 | const version = semver.clean(release?.tag_name || "v1.0.0") || "v1.6.0"; 14 | console.log(url); 15 | const test = await invoke("install_backend", { 16 | backendUrl: url, 17 | }); 18 | if (!test.includes("ERROR")) { 19 | setStandaloneVersion(version); 20 | } 21 | return; 22 | } 23 | -------------------------------------------------------------------------------- /backend/tauriMethods/downloadTemplate.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api/tauri"; 2 | 3 | export async function downloadTemplate(name: string) { 4 | return await invoke("download_template", { templateName: name }); 5 | } 6 | -------------------------------------------------------------------------------- /backend/tauriMethods/getOS.ts: -------------------------------------------------------------------------------- 1 | export async function getOS(): Promise { 2 | const { platform } = await import("@tauri-apps/api/os"); 3 | return await platform(); 4 | } 5 | -------------------------------------------------------------------------------- /backend/tauriMethods/getStandaloneVersion.ts: -------------------------------------------------------------------------------- 1 | import { readTextFile, BaseDirectory } from "@tauri-apps/api/fs"; 2 | export async function getStandaloneVersion() { 3 | const version = await readTextFile("standaloneVersion.txt", { 4 | dir: BaseDirectory.AppData, 5 | }).catch((err) => { 6 | return false; 7 | }); 8 | return version; 9 | } 10 | -------------------------------------------------------------------------------- /backend/tauriMethods/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getStandaloneVersion"; 2 | export * from "./killBackend"; 3 | export * from "./setStandaloneVersion"; 4 | export * from "./startBackend"; 5 | export * from "./checkIfStandaloneBackendExists"; 6 | export * from "./copyBackend"; 7 | export * from "./getOS"; 8 | export * from "./downloadTemplate"; 9 | export * from "./downloadBackend"; 10 | export * from "./checkIfThemeExists"; 11 | -------------------------------------------------------------------------------- /backend/tauriMethods/killBackend.ts: -------------------------------------------------------------------------------- 1 | export async function killBackend() { 2 | const { invoke } = await import("@tauri-apps/api"); 3 | return await invoke("kill_standalone_backend", {}); 4 | } 5 | -------------------------------------------------------------------------------- /backend/tauriMethods/setStandaloneVersion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | writeTextFile, 3 | BaseDirectory, 4 | createDir, 5 | exists, 6 | } from "@tauri-apps/api/fs"; 7 | export async function setStandaloneVersion(value: string) { 8 | const appDataExists = await exists("", { dir: BaseDirectory.AppData }); 9 | if (!appDataExists) { 10 | console.log("AppData dir does not exist! Creating."); 11 | await createDir("", { dir: BaseDirectory.AppData }); 12 | } 13 | writeTextFile("standaloneVersion.txt", value, { dir: BaseDirectory.AppData }); 14 | } 15 | -------------------------------------------------------------------------------- /backend/tauriMethods/startBackend.ts: -------------------------------------------------------------------------------- 1 | import { dummyFunction } from "backend/pythonMethods"; 2 | import { wrappedInvoke } from "backend/wrappedInvoke"; 3 | export async function startBackend() { 4 | const isRunning = await dummyFunction(); 5 | if (!isRunning) { 6 | return await wrappedInvoke("startBackend", [ 7 | "Start-Process", 8 | "-FilePath", 9 | "([Environment]::GetFolderPath('Startup')", 10 | "+", 11 | "'\\CssLoader-Standalone-Headless.exe')", 12 | ]); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/tauriMethods/test.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api/tauri"; 2 | 3 | export async function test() { 4 | const test = await invoke("start_backend", {}); 5 | console.log("TEST", test); 6 | } 7 | -------------------------------------------------------------------------------- /backend/toast.ts: -------------------------------------------------------------------------------- 1 | import { toast as reactToast } from "react-toastify"; 2 | export function toast(title: string, message?: string) { 3 | reactToast(`${title}${message ? ` - ${message}` : ""}`); 4 | } 5 | -------------------------------------------------------------------------------- /backend/webFetches/checkForNewBackend.ts: -------------------------------------------------------------------------------- 1 | import { getStandaloneVersion } from "../"; 2 | import { fetchNewestBackend } from "./fetchNewestBackend"; 3 | import semver from "semver"; 4 | export async function checkForNewBackend(): Promise { 5 | const current = await getStandaloneVersion(); 6 | const remote = await fetchNewestBackend(); 7 | const remoteVersion = remote?.tag_name; 8 | // This returns remoteVersion because if it's not valid, it means your current install borked 9 | if (!current || typeof current !== "string") return remoteVersion; 10 | if (!semver.valid(current)) return remoteVersion; 11 | // This is after ensuring you have a standaloneVersion.txt 12 | if (!remote) return false; 13 | if (!semver.valid(remoteVersion)) return false; 14 | if (semver.gt(remoteVersion, current)) { 15 | return remoteVersion; 16 | } 17 | return false; 18 | } 19 | -------------------------------------------------------------------------------- /backend/webFetches/fetchNewestBackend.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from "@tauri-apps/api/http"; 2 | export async function fetchNewestBackend() { 3 | return await fetch( 4 | "https://api.github.com/repos/suchmememanyskill/SDH-CssLoader/releases/latest" 5 | ) 6 | .then((res) => { 7 | return res.data; 8 | }) 9 | .then((json) => { 10 | if (json) { 11 | return json; 12 | } 13 | return; 14 | }) 15 | .catch((err) => { 16 | console.error("Error Fetching Latest Backend From Github!", err); 17 | return; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /backend/webFetches/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fetchNewestBackend"; 2 | export * from "./checkForNewBackend"; 3 | -------------------------------------------------------------------------------- /backend/wrappedInvoke.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@tauri-apps/api/shell"; 2 | 3 | export function wrappedInvoke(commandName: string, commandArgs: string[]) { 4 | return new Promise((resolve, reject) => { 5 | const command = new Command(commandName, commandArgs); 6 | command.on("close", (args: any) => resolve(args)); 7 | command.on("error", (args: any) => reject(args)); 8 | command.spawn(); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /components/AppRoot.tsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer } from "react-toastify"; 2 | import { MainNav } from "./Nav"; 3 | import { DownloadBackendPage } from "./DownloadBackendPage"; 4 | import { AppProps } from "next/app"; 5 | import { useContext, useEffect, useRef } from "react"; 6 | import { fontContext } from "@contexts/FontContext"; 7 | import { BackendFailedPage } from "./BackendFailedPage"; 8 | import { backendStatusContext } from "@contexts/backendStatusContext"; 9 | import { themeContext } from "@contexts/themeContext"; 10 | import { osContext } from "@contexts/osContext"; 11 | import { useRouter } from "next/router"; 12 | 13 | export function AppRoot({ Component, pageProps }: AppProps) { 14 | const router = useRouter(); 15 | 16 | const { montserrat, openSans } = useContext(fontContext); 17 | const { isWindows } = useContext(osContext); 18 | const { 19 | dummyResult, 20 | backendExists, 21 | showNewBackendPage, 22 | newBackendVersion, 23 | setNewBackend, 24 | setShowNewBackend, 25 | } = useContext(backendStatusContext); 26 | 27 | const { refreshThemes } = useContext(themeContext); 28 | 29 | async function onUpdateFinish() { 30 | refreshThemes(); 31 | setShowNewBackend(false); 32 | setNewBackend(""); 33 | } 34 | 35 | const scrollableContainerRef = useRef(null); 36 | 37 | useEffect(() => { 38 | if (scrollableContainerRef.current) { 39 | scrollableContainerRef.current.scrollTop = 0; 40 | } 41 | }, [router.asPath]); 42 | 43 | return ( 44 | // overflow-hidden rounded-b-lg 45 |
46 |
47 |
57 | 70 | {dummyResult && } 71 |
78 | {isWindows && (showNewBackendPage || (!backendExists && !dummyResult)) && ( 79 | setShowNewBackend(false)} 83 | backendVersion={newBackendVersion} 84 | /> 85 | )} 86 | {dummyResult ? ( 87 | <> 88 | 89 | 90 | ) : ( 91 | 92 | )} 93 |
94 |
95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /components/BackendFailedPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useContext } from "react"; 2 | import { startBackend } from "../backend"; 3 | import Image from "next/image"; 4 | import { backendStatusContext } from "@contexts/backendStatusContext"; 5 | import { osContext } from "@contexts/osContext"; 6 | 7 | function BackendLoadingTagline() { 8 | const [tagline, setTagline] = useState(""); 9 | const taglines: string[] = [ 10 | `Creating a color pallete...`, 11 | `Busy setting gradients...`, 12 | `Preparing the store now...`, 13 | `Breaking all of your settings...`, 14 | `Waking up the backend...`, 15 | `Janking up Steam...`, 16 | `Switching font-fancy and fancy-font around...`, 17 | `Spilling paint all over...`, 18 | `Inspecting class names...`, 19 | `Loading the CSS...`, 20 | `Rounding the corners...`, 21 | `Making the memes extra such...`, 22 | ]; 23 | 24 | useEffect(() => { 25 | const interval = setInterval(getTagline, 2500); 26 | 27 | return () => { 28 | clearInterval(interval); 29 | }; 30 | }, []); 31 | 32 | const getTagline = () => { 33 | const i = Math.floor(Math.random() * taglines.length); 34 | setTagline(taglines[i]); 35 | }; 36 | 37 | return <>{tagline &&

{tagline}

}; 38 | } 39 | 40 | export function BackendFailedPage() { 41 | const { isWindows } = useContext(osContext); 42 | const { recheckDummy } = useContext(backendStatusContext); 43 | const [hasWaited, setWaited] = useState(false); 44 | const [canRestart, setCanRestart] = useState(true); 45 | useEffect(() => { 46 | recheckDummy(); 47 | setTimeout(() => { 48 | setWaited(true); 49 | }, 10000); 50 | }, []); 51 | 52 | async function forceRestart() { 53 | if (canRestart) { 54 | setCanRestart(true); 55 | startBackend(); 56 | } 57 | } 58 | return ( 59 | <> 60 |
61 |
62 | CSSLoader Logo 70 |

Welcome to CSSLoader

71 | {isWindows ? ( 72 | 73 | ) : ( 74 | 75 | CSSLoader Desktop could not communicate with the backend. Please ensure you have{" "} 76 | { 79 | const { open } = await import("@tauri-apps/api/shell"); 80 | await open("https://docs.deckthemes.com/CSSLoader/Install/#linux-or-steam-deck"); 81 | }} 82 | > 83 | followed the instructions and installed it 84 | 85 | . 86 | 87 | )} 88 |
89 | {isWindows && ( 90 | 98 | )} 99 |
100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /components/CreatePresetModal.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { themeContext } from "@contexts/themeContext"; 3 | import { changePreset, checkIfThemeExists, generatePreset, setThemeState, toast } from "../backend"; 4 | import { InputAlertDialog } from "./Primitives/InputAlertDialog"; 5 | import { backendStatusContext } from "@contexts/backendStatusContext"; 6 | 7 | export function CreatePresetModal({ closeModal }: { closeModal: () => void }) { 8 | const { themes: localThemeList, refreshThemes, selectedPreset } = useContext(themeContext); 9 | const { backendManifestVersion } = useContext(backendStatusContext); 10 | 11 | const nameContainsInvalidCharacters = (presetName: string) => 12 | !!presetName.match(/[\\/:*?\"<>|]/g); 13 | 14 | const invalidName = (presetName: string) => { 15 | return ( 16 | (presetName.length === 3 || presetName.length === 4) && 17 | !!presetName.match(/(LPT\d)|(CO(M\d|N))|(NUL)|(AUX)|(PRN)/g) 18 | ); 19 | }; 20 | 21 | async function createPreset(input: string) { 22 | if (input) { 23 | const alreadyExists = await checkIfThemeExists(input); 24 | if (alreadyExists) { 25 | toast("Theme Already Exists!"); 26 | return; 27 | } 28 | await generatePreset(input); 29 | await refreshThemes(true); 30 | // Don't need to disable all themes here because the preset was created based on what you have enabled. 31 | if (selectedPreset) { 32 | await setThemeState(selectedPreset.name, false); 33 | } 34 | await setThemeState(backendManifestVersion >= 9 ? input + ".profile" : input, true); 35 | await refreshThemes(); 36 | toast("Preset Created Successfully"); 37 | closeModal(); 38 | } 39 | } 40 | return ( 41 | <> 42 | { 46 | if (!open) closeModal(); 47 | }} 48 | validateInput={(text: string) => { 49 | return !( 50 | text.length === 0 || 51 | text === "New Profile" || 52 | nameContainsInvalidCharacters(text) || 53 | invalidName(text) 54 | ); 55 | }} 56 | actionText="Create" 57 | onAction={createPreset} 58 | title="Create Profile" 59 | description={`A profile saves the current state of your themes so that you can quickly re-apply it.`} 60 | labelText="Profile Name" 61 | inputClass="bg-base-5.5-light dark:bg-base-5.5-dark" 62 | /> 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/DownloadBackendPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { downloadBackend } from "../backend/tauriMethods"; 3 | import { GenericInstallBackendModal } from "./GenericInstallBackendModal"; 4 | 5 | export function DownloadBackendPage({ 6 | onboarding = false, 7 | hideWindow, 8 | backendVersion, 9 | onUpdateFinish, 10 | }: { 11 | onboarding?: boolean; 12 | hideWindow?: any; 13 | backendVersion?: string; 14 | onUpdateFinish?: any; 15 | }) { 16 | const [installProg, setInstallProg] = useState(0); 17 | const [installText, setInstallText] = useState(""); 18 | async function installBackend() { 19 | setInstallProg(1); 20 | setInstallText("Downloading Backend"); 21 | await downloadBackend(); 22 | setInstallProg(100); 23 | setInstallText("Install Complete"); 24 | setTimeout(() => { 25 | onUpdateFinish(); 26 | }, 1000); 27 | } 28 | 29 | return ( 30 | <> 31 | 0 || onboarding} 34 | descriptionText={ 35 | onboarding ? ( 36 | <> 37 | 38 | You must install CSSLoader's Backend to use CSSLoader Desktop. If you wish to use 39 | custom images and fonts, you must{" "} 40 | { 43 | const { open } = await import("@tauri-apps/api/shell"); 44 | open("https://docs.deckthemes.com/CSSLoader/Install/#standalone"); 45 | }} 46 | > 47 | enable Windows Developer Mode. 48 | 49 | 50 | 51 | ) : ( 52 | "We recommend installing backend updates as soon as they're available in order to maintain compatibility with new themes." 53 | ) 54 | } 55 | {...{ installProg, installText }} 56 | onAction={() => installBackend()} 57 | onCloseWindow={() => hideWindow()} 58 | /> 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /components/GenericInstallBackendModal.tsx: -------------------------------------------------------------------------------- 1 | import { ImSpinner5 } from "react-icons/im"; 2 | import { AlertDialog } from "./Primitives"; 3 | import { ReactNode } from "react"; 4 | 5 | export function GenericInstallBackendModal({ 6 | titleText, 7 | installProg, 8 | installText, 9 | dontClose, 10 | onAction = () => {}, 11 | descriptionText, 12 | onCloseWindow = () => {}, 13 | Trigger = null, 14 | }: { 15 | titleText: string; 16 | Trigger?: ReactNode; 17 | // Install prog is an arbitrary number, not a real "progress percent", it just need to be >0 to convey an install is underway 18 | installProg: number; 19 | installText: string; 20 | onAction?: () => void; 21 | dontClose?: boolean; 22 | onCloseWindow?: () => void; 23 | descriptionText?: string | ReactNode; 24 | }) { 25 | return ( 26 | <> 27 | 0} 33 | dontClose={dontClose} 34 | onOpenChange={(open) => { 35 | if (!open) { 36 | onCloseWindow(); 37 | } 38 | }} 39 | title={titleText} 40 | description={descriptionText} 41 | actionText={ 42 | installProg > 0 ? ( 43 | <> 44 |
45 |
46 | 47 | {installText} 48 |
49 |
50 | 51 | ) : ( 52 | "Install" 53 | ) 54 | } 55 | /> 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /components/ManageThemes/ManageThemeCard.tsx: -------------------------------------------------------------------------------- 1 | import { BsFillCloudDownloadFill, BsTrashFill } from "react-icons/bs"; 2 | import { Flags, MinimalCSSThemeInfo, Theme } from "../../ThemeTypes"; 3 | import { LocalThemeStatus } from "../../pages/manage-themes"; 4 | import { UpdateStatus } from "../../logic"; 5 | 6 | export function ManageThemeCard({ 7 | themeData, 8 | updateStatuses, 9 | uninstalling, 10 | handleUninstall, 11 | handleUpdate, 12 | }: { 13 | themeData: Theme; 14 | updateStatuses: UpdateStatus[]; 15 | uninstalling: boolean; 16 | handleUninstall: any; 17 | handleUpdate: any; 18 | }) { 19 | // This finds the remote entry for the current theme (if it exists), and sets the data accordingly 20 | let [updateStatus, remoteEntry]: [LocalThemeStatus, false | MinimalCSSThemeInfo] = [ 21 | "installed", 22 | false, 23 | ]; 24 | const themeArrPlace = updateStatuses.find((f) => f[0] === themeData.id); 25 | if (themeArrPlace) { 26 | updateStatus = themeArrPlace[1]; 27 | remoteEntry = themeArrPlace[2]; 28 | } 29 | return ( 30 |
31 |
32 | {themeData.name} 33 | 34 | {themeData.flags.includes(Flags.isPreset) ? ( 35 | Profile 36 | ) : ( 37 | <> 38 | {themeData.version} 39 | {themeData.author ? ` • ${themeData.author}` : ""} 40 | {updateStatus === "local" ? ( 41 | - Local Theme 42 | ) : ( 43 | "" 44 | )} 45 | 46 | )} 47 | 48 |
49 |
50 | {updateStatus === "outdated" && remoteEntry && ( 51 | 58 | )} 59 |
60 | 67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /components/ManageThemes/ThemeErrorsList.tsx: -------------------------------------------------------------------------------- 1 | import { themeContext } from "@contexts/themeContext"; 2 | import { useContext } from "react"; 3 | 4 | export function ThemeErrorsList() { 5 | const { errors } = useContext(themeContext); 6 | return ( 7 |
8 |

Errors

9 |
10 | {errors.map((e) => { 11 | const [themeName, error] = e; 12 | return ( 13 |
14 |
15 | {themeName} 16 | {error} 17 |
18 |
19 | ); 20 | })} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/ManageThemes/YourProfilesList.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ManageThemeCard } from "./ManageThemeCard"; 3 | import { themeContext } from "@contexts/themeContext"; 4 | import { LocalThemeStatus } from "@pages/manage-themes"; 5 | import { Flags, MinimalCSSThemeInfo, Theme } from "ThemeTypes"; 6 | import { deletePreset } from "backend"; 7 | 8 | export function YourProfilesList({ 9 | updateStatuses, 10 | uninstalling, 11 | handleUninstall, 12 | handleUpdate, 13 | }: { 14 | updateStatuses: [string, LocalThemeStatus, false | MinimalCSSThemeInfo][]; 15 | uninstalling: boolean; 16 | handleUninstall: (e: Theme) => void; 17 | handleUpdate: (e: MinimalCSSThemeInfo) => void; 18 | }) { 19 | const { themes, refreshThemes } = useContext(themeContext); 20 | const userChangeablePresets = themes.filter((e) => e.flags.includes(Flags.isPreset)); 21 | if (userChangeablePresets.length === 0) return null; 22 | return ( 23 | <> 24 |
25 |

26 | Your Profiles 27 |

28 |
29 | {userChangeablePresets.map((e) => ( 30 | { 35 | deletePreset(e.name, themes, refreshThemes); 36 | }} 37 | handleUpdate={handleUpdate} 38 | /> 39 | ))} 40 |
41 |
42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/ManageThemes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ManageThemeCard"; 2 | export * from "./YourProfilesList"; 3 | -------------------------------------------------------------------------------- /components/Native/AppFrame.tsx: -------------------------------------------------------------------------------- 1 | import { osContext } from "@contexts/osContext"; 2 | import { useContext } from "react"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function AppFrame({ children }: { children: any }) { 6 | const { maximized } = useContext(osContext); 7 | return ( 8 |
14 | {children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Native/DynamicTitlebar.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | // @ts-ignore 4 | const DynamicTitleBar = dynamic(() => import("./Titlebar"), { 5 | ssr: false, 6 | }); 7 | 8 | export default DynamicTitleBar; -------------------------------------------------------------------------------- /components/Native/Titlebar.tsx: -------------------------------------------------------------------------------- 1 | import { appWindow } from "@tauri-apps/api/window"; 2 | import { 3 | VscChromeClose, 4 | VscChromeMaximize, 5 | VscChromeMinimize, 6 | VscChromeRestore, 7 | } from "react-icons/vsc"; 8 | import { useContext } from "react"; 9 | import Image from "next/image"; 10 | import Link from "next/link"; 11 | import * as Portal from "@radix-ui/react-portal"; 12 | import { twMerge } from "tailwind-merge"; 13 | import { osContext } from "@contexts/osContext"; 14 | 15 | const Titlebar = () => { 16 | const { fullscreen, maximized } = useContext(osContext); 17 | 18 | return ( 19 | !fullscreen && ( 20 | <> 21 | 22 |
29 |
30 | 36 | CSSLoader Logo 44 | CSSLoader Logo 52 | 53 |
54 |
55 | {/* window icons */} 56 |
appWindow.minimize()} 59 | > 60 | 61 |
62 | {maximized ? ( 63 |
appWindow.toggleMaximize()} 66 | > 67 | 68 |
69 | ) : ( 70 |
appWindow.toggleMaximize()} 73 | > 74 | 75 |
76 | )} 77 |
appWindow.close()} 80 | > 81 | 82 |
83 |
84 |
85 |
86 | 87 | ) 88 | ); 89 | }; 90 | 91 | export default Titlebar; 92 | -------------------------------------------------------------------------------- /components/Nav/MainNav.tsx: -------------------------------------------------------------------------------- 1 | import { NavTab } from "./NavTab"; 2 | import { RiArrowLeftLine, RiArrowRightLine, RiPaintFill, RiSettings2Fill } from "react-icons/ri"; 3 | import { AiOutlineCloudDownload } from "react-icons/ai"; 4 | import { BsFolder } from "react-icons/bs"; 5 | import { useState, useEffect } from "react"; 6 | import { useRouter } from "next/router"; 7 | 8 | export function MainNav() { 9 | const router = useRouter(); 10 | const [scrollPosition, setScrollPosition] = useState(0); 11 | const [containerWidth, setContainerWidth] = useState(0); 12 | const [contentWidth, setContentWidth] = useState(0); 13 | 14 | useEffect(() => { 15 | const handleResize = () => { 16 | const container = document.getElementById("navContainer"); 17 | const content = document.getElementById("navContent"); 18 | if (container && content) { 19 | setContainerWidth(container.offsetWidth); 20 | setContentWidth(content.offsetWidth); 21 | } 22 | }; 23 | 24 | handleResize(); 25 | window.addEventListener("resize", handleResize); 26 | return () => window.removeEventListener("resize", handleResize); 27 | }, []); 28 | 29 | const handleScrollLeft = () => { 30 | const minScrollPosition = 0; 31 | const maxScrollPosition = containerWidth - contentWidth; 32 | const windowWidth = window.innerWidth; 33 | 34 | setScrollPosition((prevPosition) => { 35 | const newPosition = prevPosition - windowWidth; 36 | return Math.max(minScrollPosition, newPosition); 37 | }); 38 | }; 39 | 40 | const handleScrollRight = () => { 41 | const minScrollPosition = 0; 42 | const maxScrollPosition = containerWidth - contentWidth; 43 | const windowWidth = window.innerWidth; 44 | 45 | setScrollPosition((prevPosition) => { 46 | const newPosition = prevPosition + windowWidth; 47 | return Math.min(maxScrollPosition, newPosition); 48 | }); 49 | }; 50 | 51 | return ( 52 | <> 53 | {/* 100% - 22px rather than 26px because of the added 4px margin when on /store */} 54 |
60 |
61 | {/*
62 | 66 | CSSLoader Logo 73 |

CSSLoader

74 | 75 |
*/} 76 | 139 |
140 |
141 | 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /components/Nav/NavTab.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { KeyboardEvent, useEffect } from "react"; 3 | 4 | export function NavTab({ href, name, icon }: { href: string; name: string; icon: any }) { 5 | const router = useRouter(); 6 | 7 | useEffect(() => { 8 | router.prefetch("/store"); 9 | }, []); 10 | 11 | const handleKeyDown = (e: KeyboardEvent, href: string) => { 12 | if (e.key === "Enter") { 13 | router.push(href); 14 | } 15 | }; 16 | 17 | return ( 18 | <> 19 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/Nav/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./NavTab"; 2 | export * from "./MainNav"; 3 | -------------------------------------------------------------------------------- /components/Presets/PresetSelectionDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo, useState } from "react"; 2 | import { CreatePresetModal, RadioDropdown, Tooltip } from ".."; 3 | import { themeContext } from "@contexts/themeContext"; 4 | import { Flags } from "ThemeTypes"; 5 | import { changePreset, deletePreset, setThemeState } from "backend"; 6 | import { MenuDropdown } from "@components/Primitives/MenuDropdown"; 7 | import { BiTrash } from "react-icons/bi"; 8 | import { twMerge } from "tailwind-merge"; 9 | 10 | export function PresetSelectionDropdown() { 11 | const { themes, refreshThemes, selectedPreset } = useContext(themeContext); 12 | const presets = useMemo(() => themes.filter((e) => e.flags.includes(Flags.isPreset)), [themes]); 13 | const [showModal, setShowModal] = useState(false); 14 | 15 | return ( 16 | <> 17 |
18 | {showModal && setShowModal(false)} />} 19 | e.flags.includes(Flags.isPreset) && e.enabled).length > 1 25 | ? "Invalid State" 26 | : selectedPreset?.display_name || selectedPreset?.name || "None" 27 | } 28 | options={[ 29 | // This just ensures that default profile is the first result 30 | ...(themes.filter((e) => e.flags.includes(Flags.isPreset) && e.enabled).length > 1 31 | ? ["Invalid State"] 32 | : []), 33 | "None", 34 | ...presets.map((e) => e?.display_name || e.name), 35 | "New Profile", 36 | ]} 37 | onValueChange={async (e) => { 38 | if (e === "New Profile") { 39 | setShowModal(true); 40 | return; 41 | } 42 | if (e === "Invalid State") return; 43 | if (e === "None") { 44 | await changePreset(e, themes); 45 | } 46 | // since e is the display_name, and toggle uses the real name, need to find that... 47 | // Still checks name as a fallback 48 | const themeEntry = themes.find((f) => f?.display_name === e || f.name === e); 49 | if (themeEntry) { 50 | await changePreset(themeEntry.name, themes); 51 | } 52 | refreshThemes(); 53 | }} 54 | /> 55 | 56 | , 63 | onSelect: async () => { 64 | deletePreset(selectedPreset!.name, themes, refreshThemes); 65 | }, 66 | }, 67 | ]} 68 | triggerClass={twMerge( 69 | "h-12 w-12 self-end rounded-xl border-2 border-borders-base1-dark bg-base-5.5-dark transition-all hover:border-borders-base2-dark", 70 | !selectedPreset && "opacity-50" 71 | )} 72 | /> 73 |
74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /components/Presets/PresetThemeNameDisplayCard.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { themeContext } from "@contexts/themeContext"; 3 | import { BsXCircle } from "react-icons/bs"; 4 | import { generatePresetFromThemeNames } from "backend"; 5 | import { Tooltip } from ".."; 6 | 7 | // This is the card that goes inside the PresetFolderView to say the name of one theme that is a dependency of it 8 | // Also has the remove from preset button 9 | export function PresetThemeNameDisplayCard({ themeName }: { themeName: string }) { 10 | const { selectedPreset, refreshThemes } = useContext(themeContext); 11 | return ( 12 |
13 | {themeName} 14 | Remove from preset} 17 | triggerContent={ 18 | <> 19 | { 22 | if (selectedPreset) { 23 | generatePresetFromThemeNames( 24 | selectedPreset.name, 25 | selectedPreset.dependencies.filter((e) => e !== themeName) 26 | ).then(() => { 27 | refreshThemes(); 28 | }); 29 | } 30 | }} 31 | /> 32 | 33 | } 34 | /> 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/Presets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PresetSelectionDropdown"; 2 | export * from "./PresetThemeNameDisplayCard"; 3 | -------------------------------------------------------------------------------- /components/Primitives/AlertDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as AD from "@radix-ui/react-alert-dialog"; 2 | import { fontContext } from "@contexts/FontContext"; 3 | import { ReactNode, useContext, useState, useEffect, ReactElement } from "react"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | export function AlertDialog({ 7 | Trigger = null, 8 | title, 9 | description, 10 | Content = null, 11 | Footer = null, 12 | defaultOpen = false, 13 | triggerDisabled = false, 14 | cancelText, 15 | actionText, 16 | onOpenChange = () => {}, 17 | dontCloseOnAction = false, 18 | actionDisabled = false, 19 | dontClose = false, 20 | onAction = () => {}, 21 | CustomAction = null, 22 | actionClass = "", 23 | }: { 24 | Trigger?: ReactNode; 25 | title: string; 26 | triggerDisabled?: boolean; 27 | description?: string | ReactNode; 28 | Content?: ReactNode; 29 | dontClose?: boolean; 30 | Footer?: ReactNode; 31 | dontCloseOnAction?: boolean; 32 | cancelText?: string; 33 | defaultOpen?: boolean; 34 | onOpenChange?: (open: boolean) => void; 35 | actionText?: string | ReactNode; 36 | actionDisabled?: boolean; 37 | CustomAction?: ReactNode | null; 38 | onAction?: () => void; 39 | actionClass?: string; 40 | }) { 41 | const { montserrat } = useContext(fontContext); 42 | const [open, setOpen] = useState(false); 43 | 44 | // This is here to fix a hydration error inherent to Radix 45 | useEffect(() => { 46 | setOpen(defaultOpen); 47 | }, []); 48 | 49 | return ( 50 | { 53 | if (!dontClose) { 54 | setOpen(open); 55 | onOpenChange(open); 56 | } 57 | }} 58 | > 59 | {Trigger && ( 60 | 64 | {Trigger} 65 | 66 | )} 67 | 68 |
69 | 70 | 71 |
72 | {title} 73 |
74 | {description && {description}} 75 | {Content} 76 |
77 | {!dontClose && ( 78 | 81 | {cancelText || "Dismiss"} 82 | 83 | )} 84 | {Footer} 85 | {CustomAction || ( 86 | { 88 | dontCloseOnAction && event.preventDefault(); 89 | !actionDisabled && onAction(); 90 | }} 91 | className={twMerge( 92 | "font-fancy my-2 mx-2 ml-auto rounded-2xl p-2 px-6", 93 | dontClose ? "ml-2 w-full" : "", 94 | !actionDisabled ? "bg-brandBlue" : "bg-base-5.5-dark", 95 | actionClass 96 | )} 97 | disabled={actionDisabled} 98 | > 99 | {actionText} 100 | 101 | )} 102 |
103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /components/Primitives/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | import { useMemo, useContext, useRef, useState } from "react"; 3 | import { MdKeyboardArrowDown } from "react-icons/md"; 4 | import { twMerge } from "tailwind-merge"; 5 | import { BsDot } from "react-icons/bs"; 6 | import { fontContext } from "@contexts/FontContext"; 7 | 8 | // This primitive accepts either an array of {value: string, displayName: string, bubbleValue: string | number} 9 | // Or, just an array of {value: string} 10 | // Doubly-Or, just a string array 11 | // This allows for ones with custom display text, or just simple ones 12 | // TODO: allow this to just accept a single string arr and still work 13 | 14 | export function RadioDropdown({ 15 | options, 16 | value, 17 | onValueChange, 18 | triggerClass = "", 19 | headingText, 20 | headingClass = "", 21 | ariaLabel = "", 22 | }: { 23 | options: 24 | | { 25 | value: string; 26 | displayText?: string; 27 | bubbleValue?: string | number; 28 | disabled?: boolean; 29 | }[] 30 | | string[]; 31 | value: string; 32 | onValueChange: (e: string) => void; 33 | triggerClass?: string; 34 | headingText?: string; 35 | headingClass?: string; 36 | ariaLabel: string; 37 | }) { 38 | const { montserrat } = useContext(fontContext); 39 | const [boundary, setBoundary] = useState(null); 40 | 41 | const formattedOptions = useMemo(() => { 42 | if (typeof options[0] === "string") { 43 | return options.map((e) => ({ 44 | value: e, 45 | displayText: e, 46 | bubbleValue: undefined, 47 | disabled: false, 48 | })); 49 | } 50 | // God they need to hook typescript up to a brain interface so it can learn THIS IS INTENDED 51 | // TODO: figure out the typerrors 52 | return options.map((e) => ({ 53 | // @ts-ignore 54 | value: e.value, 55 | // @ts-ignore 56 | displayText: e?.displayText || e.value, 57 | // @ts-ignore 58 | bubbleValue: e?.bubbleValue ?? undefined, 59 | // @ts-ignore 60 | disabled: e?.disabled ?? false, 61 | })); 62 | }, [options]); 63 | 64 | const selected = useMemo( 65 | () => formattedOptions.find((e: any) => e.value === value) || formattedOptions[0], 66 | [formattedOptions, value] 67 | ); 68 | return ( 69 | 70 |
71 | {headingText && ( 72 |
73 | {headingText} 74 |
75 | )} 76 | 80 |
86 |
87 | {selected?.displayText || selected?.value} 88 | {formattedOptions.reduce( 89 | (prev, cur) => (cur?.bubbleValue || prev ? true : false), 90 | false 91 | ) && ( 92 | 93 | {selected?.bubbleValue} 94 | 95 | )} 96 |
97 | 98 |
99 |
100 |
101 | 102 | 103 | {/* hot take, i actually think that forcing the dropdowns to be in dark mode has better contrast */} 104 |
109 | {/* bg-base-3-light dark:bg-base-3-dark w-[250px] text-black dark:text-white rounded-xl border-2 border-borders-base2-light dark:border-borders-base2-dark */} 110 | e.preventDefault()} 115 | className="font-fancy radio-dropdown z-[9999] my-1 h-max w-[250px] cursor-default select-none overflow-hidden overflow-y-auto rounded-xl bg-base-3-light text-sm text-black transition-all dark:bg-base-3-dark dark:text-white" 116 | > 117 | 118 |
119 | {formattedOptions.map((e) => ( 120 | 126 | 127 | 128 | 129 |
130 | 136 | {e.displayText} 137 | 138 | {e.bubbleValue !== undefined && ( 139 | {e.bubbleValue} 140 | )} 141 |
142 |
143 | ))} 144 |
145 |
146 |
147 |
148 |
149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /components/Primitives/InputAlertDialog.tsx: -------------------------------------------------------------------------------- 1 | import { AlertDialog } from "./AlertDialog"; 2 | import { FormEvent, ReactNode, useState } from "react"; 3 | import { LabelledInput } from "./LabelledField"; 4 | 5 | export function InputAlertDialog(props: { 6 | Trigger?: ReactNode; 7 | title: string; 8 | triggerDisabled?: boolean; 9 | description?: string; 10 | Content?: ReactNode; 11 | dontClose?: boolean; 12 | Footer?: ReactNode; 13 | dontCloseOnAction?: boolean; 14 | cancelText?: string; 15 | defaultOpen?: boolean; 16 | onOpenChange?: (open: boolean) => void; 17 | actionText?: string | ReactNode; 18 | actionDisabled?: boolean; 19 | customAction?: ReactNode; 20 | onAction: (input: string) => void; 21 | actionClass?: string; 22 | labelText: string; 23 | inputClass?: string; 24 | labelClass?: string; 25 | rootClass?: string; 26 | validateInput: (e: string) => boolean; 27 | }) { 28 | const [inputValue, setInputValue] = useState(""); 29 | 30 | const handleSubmit = (e: FormEvent) => { 31 | e.preventDefault(); 32 | if (props.validateInput(inputValue)) { 33 | props.onAction(inputValue); 34 | } 35 | }; 36 | 37 | return ( 38 | { 41 | if (props.validateInput(inputValue)) { 42 | props.onAction(inputValue); 43 | } 44 | }} 45 | actionDisabled={!props.validateInput(inputValue)} 46 | Content={ 47 |
48 |
49 | 55 |
56 |
57 | } 58 | /> 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/Primitives/LabelledField.tsx: -------------------------------------------------------------------------------- 1 | import * as Label from "@radix-ui/react-label"; 2 | import { useState, ReactNode, KeyboardEventHandler } from "react"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function LabelledInput({ 6 | label, 7 | value, 8 | onValueChange, 9 | rootClass = "", 10 | labelClass = "", 11 | placeholder = "", 12 | inputClass = "", 13 | password = false, 14 | onKeyDown = () => {}, 15 | }: { 16 | label: ReactNode | string; 17 | value: string; 18 | onValueChange: (e: string) => void; 19 | rootClass?: string; 20 | labelClass?: string; 21 | placeholder?: string; 22 | inputClass?: string; 23 | password?: boolean; 24 | onKeyDown?: KeyboardEventHandler; 25 | }) { 26 | const [type, setType] = useState<"text" | "password">(password ? "password" : "text"); 27 | return ( 28 |
29 | 30 | {label} 31 | 32 | password && setType("text")} 35 | onBlur={() => password && setType("password")} 36 | placeholder={placeholder} 37 | type={type} 38 | value={value} 39 | onChange={(e) => onValueChange(e.target.value)} 40 | className={twMerge( 41 | "h-12 w-full rounded-xl border-2 border-borders-base1-light bg-base-3-light px-2 transition-all hover:border-borders-base2-light focus:border-borders-base3-light dark:border-borders-base1-dark dark:bg-base-3-dark hover:dark:border-borders-base2-dark focus:dark:border-borders-base3-dark", 42 | inputClass 43 | )} 44 | /> 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/Primitives/MenuDropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | import { ReactNode, useContext } from "react"; 3 | import { twMerge } from "tailwind-merge"; 4 | import { GiHamburgerMenu } from "react-icons/gi"; 5 | import { fontContext } from "@contexts/FontContext"; 6 | 7 | export function MenuDropdown({ 8 | options, 9 | triggerClass = "", 10 | align = "center", 11 | triggerDisabled = false, 12 | }: { 13 | options: { 14 | disabled?: boolean; 15 | displayText: string; 16 | icon: ReactNode; 17 | onSelect: () => void; 18 | }[]; 19 | triggerClass?: string; 20 | triggerDisabled?: boolean; 21 | align?: "center" | "start" | "end"; 22 | }) { 23 | const { montserrat } = useContext(fontContext); 24 | return ( 25 | 26 | 34 | 35 | 36 | 37 | 38 |
39 | e.preventDefault()} 42 | className="radio-dropdown font-fancy z-[9999] w-[250px] cursor-default select-none overflow-hidden rounded-xl border-2 border-borders-base2-light bg-base-3-light text-sm text-black transition-all dark:border-borders-base2-dark dark:bg-base-3-dark dark:text-white" 43 | > 44 | {options.map((e) => { 45 | return ( 46 | 52 |
53 | 59 | {e.displayText} 60 | 61 | {e.icon} 62 |
63 |
64 | ); 65 | })} 66 |
67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /components/Primitives/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { fontContext } from "@contexts/FontContext"; 2 | import * as Dialog from "@radix-ui/react-dialog"; 3 | import { ReactNode, useContext } from "react"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | export function Modal({ 7 | Trigger = null, 8 | title, 9 | description, 10 | Content, 11 | Footer = null, 12 | triggerDisabled = false, 13 | actionDisabled = false, 14 | actionText = "", 15 | onAction = () => {}, 16 | defaultOpen = false, 17 | }: { 18 | Trigger?: ReactNode; 19 | title: string; 20 | defaultOpen?: boolean; 21 | triggerDisabled?: boolean; 22 | description?: string; 23 | Content: ReactNode; 24 | Footer?: ReactNode; 25 | actionDisabled?: boolean; 26 | actionText?: string; 27 | onAction?: () => void; 28 | }) { 29 | const { montserrat } = useContext(fontContext); 30 | 31 | return ( 32 | 33 | {Trigger} 34 | 35 |
36 | 37 | 38 |
39 | {title} 40 |
41 | {description && ( 42 | {description} 43 | )} 44 | {Content} 45 |
46 | 47 | 52 | 53 | {Footer} 54 | 62 | {actionText} 63 | 64 |
65 |
66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /components/Primitives/SimpleRadioDropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | import { MdKeyboardArrowDown } from "react-icons/md"; 3 | import { twMerge } from "tailwind-merge"; 4 | import { BsDot } from "react-icons/bs"; 5 | import { useState, useContext } from "react"; 6 | import { fontContext } from "@contexts/FontContext"; 7 | 8 | export function SimpleRadioDropdown({ 9 | options, 10 | value, 11 | onValueChange, 12 | triggerClass = "", 13 | }: { 14 | options: string[]; 15 | value: string; 16 | onValueChange: (e: string) => void; 17 | triggerClass?: string; 18 | }) { 19 | const { montserrat } = useContext(fontContext); 20 | const [boundary, setBoundary] = useState(null); 21 | 22 | const [selected, setSelected] = useState(value); 23 | function handleChange(newValue: string) { 24 | setSelected(newValue); 25 | onValueChange(newValue); 26 | } 27 | 28 | return ( 29 | 30 | 31 |
37 |
38 | {selected} 39 |
40 | 41 |
42 |
43 | 44 | 45 |
50 | {/* bg-base-3-light dark:bg-base-3-dark w-[250px] text-black dark:text-white rounded-xl border-2 border-borders-base2-light dark:border-borders-base2-dark */} 51 | 58 | 59 |
60 | {options.map((e) => ( 61 | 66 | 67 | 68 | 69 |
70 | {e} 71 |
72 |
73 | ))} 74 |
75 |
76 |
77 |
78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /components/Primitives/ToggleSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as Switch from "@radix-ui/react-switch"; 2 | import { useState, useEffect } from "react"; 3 | 4 | export function ToggleSwitch({ 5 | checked: propChecked, 6 | onChange, 7 | disabled = false, 8 | }: { 9 | checked?: boolean; 10 | onChange: (value: boolean) => void; 11 | disabled?: boolean; 12 | }) { 13 | // because checked is controlled by prop and by state, we have to do this 14 | const [stateChecked, setStateChecked] = useState(false); 15 | 16 | useEffect(() => { 17 | setStateChecked(propChecked ?? false); 18 | }, [propChecked]); 19 | 20 | const handleToggle = () => { 21 | const newValue = !stateChecked; 22 | setStateChecked(newValue); 23 | onChange(newValue); 24 | }; 25 | 26 | return ( 27 | handleToggle()} 31 | className="relative h-[25px] w-[42px] cursor-default rounded-full bg-base-6-dark shadow-[0_2px_10px] shadow-base-2-dark outline-none focus-visible:shadow-[0_0_0_2px] focus-visible:shadow-[hsl(43_100%_64.0%)] data-[state=checked]:bg-brandBlue" 32 | > 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /components/Primitives/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { fontContext } from "@contexts/FontContext"; 2 | import * as RadixTooltip from "@radix-ui/react-tooltip"; 3 | import { ReactElement, useContext, useState } from "react"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | export function Tooltip({ 7 | triggerContent, 8 | delayDuration = 100, 9 | tooltipSide = "bottom", 10 | contentClass = "", 11 | arrow = false, 12 | disabled = false, 13 | content, 14 | triggerRootClass = "", 15 | align = "center", 16 | }: { 17 | triggerContent: ReactElement; 18 | delayDuration?: number; 19 | tooltipSide?: "top" | "bottom" | "left" | "right"; 20 | contentClass?: string; 21 | disabled?: boolean; 22 | arrow?: boolean; 23 | content: ReactElement | string; 24 | triggerRootClass?: string; 25 | align?: "center" | "end" | "start"; 26 | }) { 27 | const { montserrat } = useContext(fontContext); 28 | const [open, setOpen] = useState(false); 29 | return ( 30 | 31 | !disabled && setOpen(open)} 34 | delayDuration={delayDuration} 35 | > 36 | {triggerContent} 37 | 38 |
39 | 47 | {arrow && } 48 | {content} 49 | 50 |
51 |
52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/Primitives/TwoItemToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, ReactNode, SetStateAction, useState } from "react"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function TwoItemToggle({ 5 | value, 6 | label, 7 | onValueChange, 8 | options, 9 | rootClass = "", 10 | optionClass = "", 11 | buttonClass = "", 12 | highlightClass = "", 13 | }: { 14 | value: any; 15 | label?: string; 16 | optionClass?: string; 17 | onValueChange: Dispatch>; 18 | options: { value: any; displayText: string | ReactNode }[]; 19 | rootClass?: string; 20 | buttonClass?: string; 21 | highlightClass?: string; 22 | }) { 23 | return ( 24 | <> 25 |
26 | {label && {label}} 27 |
33 | 42 | 51 |
58 |
59 |
60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /components/Primitives/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./LabelledField"; 2 | export * from "./SimpleRadioDropdown"; 3 | export * from "./Dropdown"; 4 | export * from "./TwoItemToggle"; 5 | export * from "./Modal"; 6 | export * from "./AlertDialog"; 7 | export * from "./Tooltip"; 8 | export * from "./ToggleSwitch"; 9 | export * from "./MenuDropdown"; 10 | -------------------------------------------------------------------------------- /components/Settings/CreateTemplateTheme.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { checkIfThemeExists, downloadTemplate, toast } from "backend"; 3 | import { ImSpinner5 } from "react-icons/im"; 4 | import { themeContext } from "@contexts/themeContext"; 5 | import { InputAlertDialog } from "@components/Primitives/InputAlertDialog"; 6 | 7 | export function CreateTemplateTheme({ ongoingAction }: { ongoingAction: boolean }) { 8 | const { refreshThemes } = useContext(themeContext); 9 | const nameContainsInvalidCharacters = (presetName: string) => 10 | !!presetName.match(/[\\/:*?\"<>|]/g); 11 | 12 | const invalidName = (presetName: string) => { 13 | return ( 14 | (presetName.length === 3 || presetName.length === 4) && 15 | !!presetName.match(/(LPT\d)|(CO(M\d|N))|(NUL)|(AUX)|(PRN)/g) 16 | ); 17 | }; 18 | return ( 19 | <> 20 | { 23 | return !(text.length === 0 || nameContainsInvalidCharacters(text) || invalidName(text)); 24 | }} 25 | onAction={async (name) => { 26 | const alreadyExists = await checkIfThemeExists(name); 27 | if (alreadyExists) { 28 | toast("Theme Already Exists!"); 29 | return; 30 | } 31 | downloadTemplate(name).then((success) => { 32 | toast(success ? "Template Created Successfully" : "Error Creating Template"); 33 | refreshThemes(true); 34 | }); 35 | }} 36 | actionText="Create" 37 | title="Create Template Theme" 38 | description={`This will create a blank theme in your themes folder that you can use as the starting point for your own theme.`} 39 | triggerDisabled={ongoingAction} 40 | Trigger={ 41 |
42 | {ongoingAction ? : "Create Template Theme"} 43 |
44 | } 45 | /> 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /components/Settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CreateTemplateTheme"; 2 | -------------------------------------------------------------------------------- /components/SingleTheme/PatchComponent.tsx: -------------------------------------------------------------------------------- 1 | import { VFC, useContext } from "react"; 2 | 3 | import { open } from "@tauri-apps/api/dialog"; 4 | import { ThemePatchComponent } from "../../ThemeTypes"; 5 | import { FaFolder } from "react-icons/fa"; 6 | import { 7 | fetchThemePath, 8 | generatePresetFromThemeNames, 9 | getInstalledThemes, 10 | setComponentOfThemePatch, 11 | toast, 12 | } from "../../backend"; 13 | import { themeContext } from "@contexts/themeContext"; 14 | 15 | export const PatchComponent = ({ 16 | data, 17 | selectedLabel, 18 | themeName, 19 | patchName, 20 | bottomSeparatorValue, 21 | }: { 22 | data: ThemePatchComponent; 23 | selectedLabel: string; 24 | themeName: string; 25 | patchName: string; 26 | bottomSeparatorValue: any; 27 | }) => { 28 | const { selectedPreset } = useContext(themeContext); 29 | function setComponentAndReload(value: string) { 30 | setComponentOfThemePatch( 31 | themeName, 32 | patchName, 33 | data.name, // componentName 34 | value 35 | ).then(() => { 36 | if (selectedPreset && selectedPreset.dependencies.includes(themeName)) { 37 | generatePresetFromThemeNames(selectedPreset.name, selectedPreset.dependencies); 38 | } 39 | getInstalledThemes(); 40 | }); 41 | } 42 | if (selectedLabel === data.on) { 43 | // The only value that changes from component to component is the value, so this can just be re-used 44 | switch (data.type) { 45 | case "image-picker": 46 | // This makes things compatible with people using HoloISO or who don't have the user /deck/ 47 | // These have to 48 | return ( 49 |
50 | 104 |
105 | ); 106 | case "color-picker": 107 | return ( 108 | <> 109 |
110 | {data.name} 111 | { 116 | setComponentAndReload(e.target.value); 117 | }} 118 | /> 119 |
120 | 121 | ); 122 | } 123 | } 124 | return null; 125 | }; 126 | -------------------------------------------------------------------------------- /components/SingleTheme/ThemePatch.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from "react"; 2 | import { Flags, Patch } from "../../ThemeTypes"; 3 | import { PatchComponent } from "./PatchComponent"; 4 | import { themeContext } from "@contexts/themeContext"; 5 | import { generatePresetFromThemeNames, setPatchOfTheme } from "../../backend"; 6 | import { SimpleRadioDropdown } from "@components/Primitives"; 7 | 8 | export function ThemePatch({ 9 | data, 10 | index, 11 | fullArr, 12 | themeName, 13 | }: { 14 | data: Patch; 15 | index: number; 16 | fullArr: Patch[]; 17 | themeName: string; 18 | }) { 19 | const { refreshThemes, selectedPreset } = useContext(themeContext); 20 | const [selectedIndex, setIndex] = useState(data.options.indexOf(data.value)); 21 | 22 | const [selectedLabel, setLabel] = useState(data.value); 23 | 24 | const bottomSeparatorValue = fullArr.length - 1 !== index; 25 | 26 | async function setPatchValue(value: string) { 27 | return setPatchOfTheme(themeName, data.name, value).then(() => { 28 | if (selectedPreset && selectedPreset.dependencies.includes(themeName)) { 29 | return generatePresetFromThemeNames(selectedPreset.name, selectedPreset.dependencies); 30 | } 31 | }); 32 | } 33 | 34 | function ComponentContainer() { 35 | return ( 36 | <> 37 | {data.components.length > 0 ? ( 38 |
39 | {data.components.map((e) => ( 40 |
41 | 48 |
49 | ))} 50 |
51 | ) : null} 52 | 53 | ); 54 | } 55 | 56 | return ( 57 | <> 58 | {(() => { 59 | switch (data.type) { 60 | case "slider": 61 | return ( 62 | <> 63 |
64 | {data.name} 65 |
66 | { 73 | const value = Number(event.target.value); 74 | setPatchValue(data.options[value]); 75 | setIndex(value); 76 | setLabel(data.options[value]); 77 | }} 78 | /> 79 |
80 | {data.options.map((e, i) => { 81 | return ( 82 |
87 | | 88 | {e} 89 |
90 | ); 91 | })} 92 |
93 |
94 |
95 | 96 | ); 97 | case "checkbox": 98 | return ( 99 | <> 100 |
101 | {data.name} 102 |