├── .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 |
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 | hasWaited && forceRestart()}
95 | >
96 | Force Restart Backend
97 |
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 |
remoteEntry && handleUpdate(remoteEntry)}
53 | className="flex flex-col items-center justify-center focus-visible:ring-4 focus-visible:ring-amber9 2cols:flex-row-reverse 2cols:gap-2"
54 | >
55 |
56 | {remoteEntry.version}
57 |
58 | )}
59 |
60 | handleUninstall(themeData)}
64 | >
65 |
66 |
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 |
44 |
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 |
73 |
CSSLoader
74 |
75 | */}
76 |
80 |
= 0 ? "pointer-events-none opacity-0" : ""
83 | }`}
84 | onClick={handleScrollLeft}
85 | disabled={scrollPosition >= 0}
86 | >
87 |
88 |
89 |
90 |
99 |
100 |
101 |
= 0
106 | ? ""
107 | : "linear-gradient(270deg, rgba(0, 0, 0, 0) 0%, rgb(9, 10, 12) 48.08%)"
108 | }`,
109 | }}
110 | >
111 |
121 |
129 |
133 |
} />
134 |
} />
135 |
} />
136 |
} />
137 |
138 |
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 | router.push(href)}
22 | onKeyDown={(e) => handleKeyDown(e, href)}
23 | style={{
24 | background: router.pathname === href ? "rgb(37, 99, 235)" : "#1e2024",
25 | }}
26 | className="flex h-fit items-center justify-center gap-2 rounded-full border-4 border-transparent bg-elevation-2-dark px-4 py-2 transition-all duration-150 hover:scale-95 focus-visible:border-amber9 hover:active:scale-90"
27 | >
28 | {icon}
29 | {name}
30 |
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 |
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 |
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 |
50 | Dismiss
51 |
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 |
onValueChange(options[0].value)}
39 | >
40 | {options[0].displayText}
41 |
42 |
onValueChange(options[1].value)}
48 | >
49 | {options[1].displayText}
50 |
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 |
53 | fetchThemePath()
54 | .then((res) => {
55 | return res.result;
56 | })
57 | .then((rootPath: string) => {
58 | open({
59 | directory: false,
60 | multiple: false,
61 | filters: [
62 | {
63 | name: "Image File",
64 | extensions: ["svg", "png", "jpg", "jpeg", "avif", "webp", "gif"],
65 | },
66 | ],
67 | defaultPath: rootPath,
68 | }).then((path) => {
69 | if (!path) {
70 | toast("Error!", "No File Selected");
71 | return;
72 | }
73 | if (typeof path === "string") {
74 | if (!path?.includes(rootPath)) {
75 | toast("Invalid File", "Images must be within themes folder");
76 | return;
77 | }
78 | if (!/\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(path)) {
79 | toast("Invalid File", "Must be an image file");
80 | return;
81 | }
82 | const relativePath = path.split(`${rootPath}`)[1].slice(1);
83 | console.log(relativePath);
84 | setComponentAndReload(relativePath);
85 | }
86 | });
87 | })
88 | }
89 | >
90 | {data.name}
91 |
101 |
102 |
103 |
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 |
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 |
103 | {
108 | const bool = event.target.checked;
109 | const newValue = bool ? "Yes" : "No";
110 | setPatchValue(newValue).then(() => {
111 | refreshThemes();
112 | });
113 | setLabel(newValue);
114 | setIndex(data.options.findIndex((e) => e === newValue));
115 | }}
116 | />
117 |
118 |
119 |
120 | >
121 | );
122 | case "dropdown":
123 | return (
124 | <>
125 |
126 | {data.name}
127 | {
131 | setPatchValue(e);
132 | setLabel(e);
133 | }}
134 | />
135 |
136 | >
137 | );
138 | case "none":
139 | return (
140 | <>
141 |
142 | {data.name}
143 |
144 | >
145 | );
146 | default:
147 | return null;
148 | }
149 | })()}
150 |
151 | {bottomSeparatorValue && (
152 |
153 | )}
154 | >
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/components/SingleTheme/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useMemo, useContext, useEffect } from "react";
2 | import { Flags, Theme } from "../../ThemeTypes";
3 | import { ThemePatch } from "./ThemePatch";
4 | import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
5 | import { themeContext } from "@contexts/themeContext";
6 | import { generatePreset, generatePresetFromThemeNames, setThemeState, toast } from "../../backend";
7 | import { AlertDialog, ToggleSwitch } from "..";
8 | import { twMerge } from "tailwind-merge";
9 |
10 | function OptionalDepsModal({
11 | themeData,
12 | closeModal,
13 | }: {
14 | themeData: Theme;
15 | closeModal: () => void;
16 | }) {
17 | const { refreshThemes, selectedPreset } = useContext(themeContext);
18 |
19 | const [enableDeps, setEnableDeps] = useState(true);
20 | const [enableDepValues, setEnableDepValues] = useState(true);
21 | useEffect(() => {
22 | if (!enableDeps) setEnableDepValues(false);
23 | }, [enableDeps]);
24 |
25 | async function enableThemeOptDeps() {
26 | await setThemeState(themeData.name, true, enableDeps, enableDepValues);
27 | await refreshThemes();
28 | if (!selectedPreset) return;
29 | generatePresetFromThemeNames(selectedPreset.name, [
30 | ...selectedPreset.dependencies,
31 | themeData.name,
32 | ]);
33 | }
34 |
35 | const handleEnableDepsToggle = (v: boolean) => {
36 | setEnableDeps(v);
37 | };
38 |
39 | const handleEnableDepValuesToggle = (v: boolean) => {
40 | setEnableDepValues(v);
41 | };
42 |
43 | return (
44 | <>
45 |
52 |
53 |
54 | Enable dependencies
55 |
56 |
57 |
62 | Enable pre-configured settings for dependencies
63 |
64 |
65 | }
66 | actionText={`Enable ${themeData.name}`}
67 | onAction={() => {
68 | enableThemeOptDeps();
69 | closeModal();
70 | }}
71 | />
72 | >
73 | );
74 | }
75 |
76 | export function ThemeToggle({
77 | data,
78 | collapsible = false,
79 | rootClass = "",
80 | }: {
81 | data: Theme;
82 | collapsible?: boolean;
83 | rootClass?: string;
84 | }) {
85 | const { refreshThemes, selectedPreset, themes } = useContext(themeContext);
86 | const [showOptDepsModal, setShowOptDepsModal] = useState(false);
87 | const [collapsed, setCollapsed] = useState(true);
88 | const isPreset = useMemo(() => {
89 | if (data.flags.includes(Flags.isPreset)) {
90 | return true;
91 | }
92 | return false;
93 | // This might not actually memoize it as data.flags is an array, so idk if it deep checks the values here
94 | }, [data.flags]);
95 |
96 | return (
97 |
103 | {showOptDepsModal && (
104 |
setShowOptDepsModal(false)} />
105 | )}
106 |
107 |
108 | {data?.display_name || data.name}
109 |
110 | {isPreset ? `Preset` : `${data.version} • ${data.author}`}
111 |
112 |
113 |
114 | <>
115 |
{
118 | // TODO: redo this!
119 |
120 | // Re-collapse menu
121 | setCollapsed(true);
122 | // If theme has optional dependency flag
123 | if (switchValue === true && data.flags.includes(Flags.optionalDeps)) {
124 | setShowOptDepsModal(true);
125 | return;
126 | }
127 | // Actually enabling the theme
128 | await setThemeState(data.name, switchValue);
129 |
130 | // Need to grab up to date data
131 | const updatedThemes: Theme[] | undefined = await refreshThemes();
132 |
133 | // Dependency Toast
134 | if (data.dependencies.length > 0) {
135 | if (switchValue) {
136 | toast(
137 | `${data.name} enabled other themes`,
138 | `${
139 | data.dependencies.length === 1
140 | ? `1 other theme is required by ${data.name}`
141 | : `${data.dependencies.length} other themes are required by ${data.name}`
142 | }`
143 | );
144 | }
145 | if (!switchValue && !data.flags.includes(Flags.dontDisableDeps)) {
146 | toast(
147 | `${data.name} disabled other themes`,
148 | // @ts-ignore
149 | `${
150 | data.dependencies.length === 1
151 | ? `1 theme was originally enabled by ${data.name}`
152 | : `${data.dependencies.length} themes were originally enabled by ${data.name}`
153 | }`
154 | );
155 | }
156 | }
157 |
158 | if (!selectedPreset || !updatedThemes) return;
159 | // This used to generate the new list of themes by the dependencies of the preset + or - the checked theme
160 | // However, since we added profiles, the list of enabled themes IS the list of dependencies, so this works
161 | await generatePresetFromThemeNames(
162 | selectedPreset.name,
163 | updatedThemes
164 | .filter((e) => e.enabled && !e.flags.includes(Flags.isPreset))
165 | .map((e) => e.name)
166 | );
167 | }}
168 | />
169 | >
170 |
171 | {data.enabled && data.patches.length > 0 && (
172 | <>
173 |
174 | {collapsible && (
175 |
176 |
177 | Theme Settings
178 |
179 | setCollapsed(!collapsed)}
183 | >
184 | {collapsed ? (
185 |
191 | ) : (
192 |
197 | )}
198 |
199 |
200 | )}
201 | {!collapsible || !collapsed ? (
202 |
203 | {data.patches.map((x, i, arr) => {
204 | return (
205 |
212 | );
213 | })}
214 |
215 | ) : null}
216 |
217 | >
218 | )}
219 |
220 | );
221 | }
222 |
--------------------------------------------------------------------------------
/components/SingleTheme/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./PatchComponent";
2 | export * from "./ThemePatch";
3 | export * from "./ThemeToggle";
4 |
--------------------------------------------------------------------------------
/components/YourThemes/OneColumnThemeView.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeToggle } from "../SingleTheme";
2 | import { Theme } from "ThemeTypes";
3 |
4 | export function OneColumnThemeView({ themes }: { themes: Theme[] }) {
5 | return (
6 |
7 | {themes.map((e) => {
8 | return ;
9 | })}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/components/YourThemes/TwoColumnThemeView.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { ThemeToggle } from "../SingleTheme";
3 | import { Theme } from "../../ThemeTypes";
4 |
5 | export function TwoColumnThemeView({ themes }: { themes: Theme[] }) {
6 | // This takes the list of themes and returns two columns
7 | // When these columns are displayed as left and right, the themes inside will read alphabetically, left ro right and top to bottom.
8 | // A B
9 | // C D
10 | // E F
11 | // etc, etc
12 | const [leftColumn, rightColumn] = useMemo(() => {
13 | let leftColumn: Theme[] = [],
14 | rightColumn: Theme[] = [];
15 | themes.sort().forEach((e, i) => {
16 | if (i % 2 === 0) {
17 | leftColumn.push(e);
18 | } else {
19 | rightColumn.push(e);
20 | }
21 | });
22 | return [leftColumn, rightColumn];
23 | }, [themes]);
24 |
25 | // If you're wondering "why not CSS grid", it's because each theme has it's own unique height
26 | // Having the left-col theme affect the right-col theme's height looked bad
27 | return (
28 |
29 |
30 |
31 | {leftColumn.map((e) => {
32 | return ;
33 | })}
34 |
35 |
36 | {rightColumn.map((e) => {
37 | return ;
38 | })}
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/YourThemes/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./TwoColumnThemeView";
2 | export * from "./OneColumnThemeView";
3 |
--------------------------------------------------------------------------------
/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./SingleTheme";
2 | export * from "./Nav";
3 | export * from "./DownloadBackendPage";
4 | export * from "./BackendFailedPage";
5 | export * from "./CreatePresetModal";
6 | export * from "./YourThemes";
7 | export * from "./ManageThemes";
8 | export * from "./Primitives";
9 | export * from "./Presets";
10 |
--------------------------------------------------------------------------------
/constants.ts:
--------------------------------------------------------------------------------
1 | export const apiUrl = "https://api.deckthemes.com";
2 | export const storeUrl = "https://deckthemes.com/desktop";
3 | // export const storeUrl = "http://localhost:3000/desktop";
4 | export const allowedStoreOrigins = [
5 | "https://deckthemes.com",
6 | "https://beta.deckthemes.com",
7 | "https://alpha.deckthemes.com",
8 | "http://localhost:3000",
9 | ];
10 |
--------------------------------------------------------------------------------
/contexts/FontContext.tsx:
--------------------------------------------------------------------------------
1 | import { Montserrat, Open_Sans } from "next/font/google";
2 | import { Children, createContext } from "react";
3 |
4 | const montserrat = Montserrat({
5 | subsets: ["latin"],
6 | variable: "--montserrat",
7 | });
8 |
9 | const openSans = Open_Sans({
10 | subsets: ["latin"],
11 | variable: "--opensans",
12 | });
13 |
14 | export const fontContext = createContext<{ montserrat: any; openSans: any }>({
15 | montserrat: "",
16 | openSans: "",
17 | });
18 | // TODO: add type def
19 | export function FontContext({ children }: { children: any }) {
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/contexts/backendStatusContext.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, createContext } from "react";
2 |
3 | export const backendStatusContext = createContext<{
4 | dummyResult: boolean | undefined;
5 | backendExists: boolean;
6 | showNewBackendPage: boolean;
7 | newBackendVersion: string;
8 | recheckDummy: any;
9 | setNewBackend: Dispatch>;
10 | setShowNewBackend: Dispatch>;
11 | backendManifestVersion: number;
12 | }>({
13 | dummyResult: undefined,
14 | showNewBackendPage: false,
15 | newBackendVersion: "",
16 | recheckDummy: () => {},
17 | backendExists: false,
18 | setNewBackend: () => {},
19 | setShowNewBackend: () => {},
20 | backendManifestVersion: 8,
21 | });
22 |
--------------------------------------------------------------------------------
/contexts/osContext.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, createContext } from "react";
2 |
3 | export const osContext = createContext<{
4 | OS: string;
5 | isWindows: boolean;
6 | maximized: boolean;
7 | fullscreen: boolean;
8 | }>({
9 | OS: "",
10 | isWindows: false,
11 | maximized: false,
12 | fullscreen: false,
13 | });
14 |
--------------------------------------------------------------------------------
/contexts/themeContext.ts:
--------------------------------------------------------------------------------
1 | import { Theme, ThemeError } from "ThemeTypes";
2 | import { createContext } from "react";
3 |
4 | export const themeContext = createContext<{
5 | themes: Theme[];
6 | setThemes: any;
7 | refreshThemes: any;
8 | selectedPreset: Theme | undefined;
9 | errors: ThemeError[];
10 | setErrors: any;
11 | }>({
12 | themes: [],
13 | setThemes: () => {},
14 | refreshThemes: () => {},
15 | selectedPreset: undefined,
16 | errors: [],
17 | setErrors: () => {},
18 | });
19 |
--------------------------------------------------------------------------------
/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useVW";
2 | export * from "./useInterval";
3 | export * from "./usePlatform";
4 | export * from "./useBasicAsyncEffect";
5 |
--------------------------------------------------------------------------------
/hooks/useBasicAsyncEffect.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | // This just runs an async function on mount, doesn't have support for debouncing the 2 calls in Strict mode, return functions, etc etc
4 | export function useBasicAsyncEffect(asyncStuff: () => any, deps: any[] = []) {
5 | useEffect(() => {
6 | asyncStuff();
7 | }, deps);
8 | }
9 |
--------------------------------------------------------------------------------
/hooks/useInterval.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 |
3 | export function useInterval(fn: () => void, interval: number) {
4 | const [active, setActive] = useState(false);
5 | const intervalRef = useRef();
6 | const fnRef = useRef<() => void>();
7 |
8 | useEffect(() => {
9 | fnRef.current = fn;
10 | }, [fn]);
11 |
12 | const start = () => {
13 | setActive((old) => {
14 | if (!old && !intervalRef.current) {
15 | // @ts-ignore
16 | intervalRef.current = window.setInterval(fnRef.current, interval);
17 | }
18 | return true;
19 | });
20 | };
21 |
22 | const stop = () => {
23 | setActive(false);
24 | window.clearInterval(intervalRef.current);
25 | intervalRef.current = undefined;
26 | };
27 |
28 | const toggle = () => {
29 | if (active) {
30 | stop();
31 | } else {
32 | start();
33 | }
34 | };
35 |
36 | return { start, stop, toggle, active };
37 | }
--------------------------------------------------------------------------------
/hooks/usePlatform.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "@tauri-apps/api/os";
2 | import { useEffect, useState } from "react";
3 |
4 | // Unused ATM
5 | // This was originally intended to be used to gate backend-installation to windows, as lord knows I can't write a systemd service to do it on linux
6 | export function usePlatform() {
7 | const [platform, setPlatform] = useState("linux");
8 | useEffect(() => {
9 | const getPlatform = async () => {
10 | const { platform: tauriPlatform } = await import("@tauri-apps/api/os");
11 | tauriPlatform().then((value) => {
12 | console.log("platform: ", value);
13 | setPlatform(value);
14 | });
15 | };
16 | getPlatform();
17 | }, []);
18 | return platform;
19 | }
20 |
--------------------------------------------------------------------------------
/hooks/useVW.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useSyncExternalStore } from "react";
2 |
3 | export function useVW2() {
4 | const [vw, setVW] = useState(window.innerWidth);
5 |
6 | useEffect(() => {
7 | let timeoutId: NodeJS.Timeout;
8 |
9 | function handleResize() {
10 | clearTimeout(timeoutId);
11 | timeoutId = setTimeout(() => {
12 | setVW(window.innerWidth);
13 | }, 100);
14 | }
15 |
16 | window.addEventListener("resize", handleResize);
17 |
18 | return () => {
19 | clearTimeout(timeoutId);
20 | window.removeEventListener("resize", handleResize);
21 | };
22 | }, []);
23 |
24 | return vw;
25 | }
26 |
27 | function subscribe(callback: any) {
28 | let timeoutId: NodeJS.Timeout;
29 |
30 | function handleResize() {
31 | clearTimeout(timeoutId);
32 | timeoutId = setTimeout(() => {
33 | callback();
34 | }, 100);
35 | }
36 |
37 | window.addEventListener("resize", handleResize);
38 | return () => window.removeEventListener("resize", handleResize);
39 | }
40 | function getSnapshot() {
41 | return window.innerWidth;
42 | }
43 |
44 | export function useVW() {
45 | const vw = useSyncExternalStore(subscribe, getSnapshot);
46 | console.log(vw);
47 | return vw;
48 | }
49 |
--------------------------------------------------------------------------------
/latest.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.2.1",
3 | "notes": "Check Github for the release notes!",
4 | "pub_date": "2024-03-15T00:31:06.252Z",
5 | "platforms": {
6 | "linux-x86_64": {
7 | "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUR25UdTJ0dzN1MXVmRFdVZUF6Z2E2NGo0eVM3N3g4dlFmSTUwT1pqWWFtM0lVbWxzSnZWVHJyeFZsVFJhbW5qYzZCVWpHTXh1K1lZaWlFc2E5WkthUHIwTDRuRWN5cWd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzEwNDYyNDYxCWZpbGU6Y3NzLWxvYWRlci1kZXNrdG9wXzEuMi4xX2FtZDY0LkFwcEltYWdlLnRhci5negpIazgwNFF5bFNsMm9GUWRHWENIL1FoYW9PMDF3U3FXOTdOZ3FwNkRMNStlcG5oekpWbTEzb3dQUmhnNkQ5clc0MXJmbGlmUDU4bG9ZUkw5MjZ4MFpDQT09Cg==",
8 | "url": "https://github.com/DeckThemes/CSSLoader-Desktop/releases/download/v1.2.1/CSSLoader.Desktop_1.2.1.AppImage.tar.gz"
9 | },
10 | "windows-x86_64": {
11 | "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUR25UdTJ0dzN1MXBCM2RDRlpaNzBaOEF6V2hBTWU5YTFVSDM2dFVrb0NjaFd5M2VHdEExd21hVDFmWWtiSnQvK1cvZGxGSEFnY2ZVQlY4dWhxaXBJOWJwWWZiSEV2RFFVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzEwNDYyNjYyCWZpbGU6Q1NTTG9hZGVyIERlc2t0b3BfMS4yLjFfeDY0X2VuLVVTLm1zaS56aXAKNmZqOG5LNG9DQ1JCZUVIWUpMNkJPenNFS0JWT1RiT2RtZVZVRTFCS1VWNzR0MHQyd2ZPYlBVOUtoaGRwc2tiRFlYaTRCL3NDbnQxYnZtT0xFWGpkQWc9PQo=",
12 | "url": "https://github.com/DeckThemes/CSSLoader-Desktop/releases/download/v1.2.1/CSSLoader.Desktop_1.2.1.msi.zip"
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/logic/bulkThemeUpdateCheck.ts:
--------------------------------------------------------------------------------
1 | import { MinimalCSSThemeInfo, Theme } from "../ThemeTypes";
2 | import { LocalThemeStatus } from "../pages/manage-themes";
3 | import { apiUrl } from "../constants";
4 | import { fetch } from "@tauri-apps/api/http";
5 |
6 | export type UpdateStatus = [
7 | string,
8 | LocalThemeStatus,
9 | false | MinimalCSSThemeInfo
10 | ];
11 |
12 | async function fetchThemeIDS(
13 | idsToQuery: string[]
14 | ): Promise {
15 | const queryStr = "?ids=" + idsToQuery.join(".");
16 | return fetch(`${apiUrl}/themes/ids${queryStr}`)
17 | .then((res) => {
18 | return res.data;
19 | })
20 | .then((data) => {
21 | if (data) return data;
22 | return [];
23 | })
24 | .catch((err) => {
25 | console.error("Error Fetching Theme Updates!", err);
26 | return [];
27 | });
28 | }
29 |
30 | export async function bulkThemeUpdateCheck(localThemeList: Theme[] = []) {
31 | let idsToQuery: string[] = localThemeList.map((e) => e.id);
32 | if (idsToQuery.length === 0) return [];
33 |
34 | const themeArr = await fetchThemeIDS(idsToQuery);
35 | if (themeArr.length === 0) return [];
36 |
37 | const updateStatusArr: UpdateStatus[] = localThemeList.map((localEntry) => {
38 | const remoteEntry = themeArr.find(
39 | (remote) => remote.id === localEntry.id || remote.name === localEntry.id
40 | );
41 | if (!remoteEntry) return [localEntry.id, "local", false];
42 | if (remoteEntry.version === localEntry.version)
43 | return [localEntry.id, "installed", remoteEntry];
44 | return [localEntry.id, "outdated", remoteEntry];
45 | });
46 |
47 | return updateStatusArr;
48 | }
49 |
--------------------------------------------------------------------------------
/logic/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./bulkThemeUpdateCheck";
2 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | const nextConfig = {
4 | output: "export",
5 | reactStrictMode: true,
6 | // Note: This feature is required to use NextJS Image in SSG mode.
7 | // See https://nextjs.org/docs/messages/export-image-api for different workarounds.
8 | images: {
9 | unoptimized: true,
10 | },
11 | };
12 |
13 | module.exports = nextConfig;
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CSSLoader-Desktop",
3 | "version": "1.2.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "export": "next export",
9 | "tauri": "tauri",
10 | "start": "next start",
11 | "lint": "next lint"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-alert-dialog": "^1.0.4",
15 | "@radix-ui/react-dialog": "^1.0.4",
16 | "@radix-ui/react-dropdown-menu": "^2.0.5",
17 | "@radix-ui/react-label": "^2.0.2",
18 | "@radix-ui/react-portal": "^1.0.3",
19 | "@radix-ui/react-switch": "^1.0.3",
20 | "@radix-ui/react-tooltip": "^1.0.6",
21 | "@tauri-apps/api": "^1.2.0",
22 | "@types/node": "18.15.10",
23 | "@types/react": "18.0.29",
24 | "@types/react-dom": "18.0.11",
25 | "@types/semver": "^7.5.0",
26 | "autoprefixer": "^10.4.14",
27 | "next": "13.2.4",
28 | "postcss": "^8.4.21",
29 | "prettier-plugin-tailwindcss": "^0.3.0",
30 | "react": "18.2.0",
31 | "react-dom": "18.2.0",
32 | "react-icons": "^4.8.0",
33 | "react-toastify": "^9.1.2",
34 | "semver": "^7.5.1",
35 | "tailwind-merge": "^1.13.2",
36 | "tailwindcss": "^3.2.7",
37 | "typescript": "5.0.2"
38 | },
39 | "devDependencies": {
40 | "@tauri-apps/cli": "^1.2.3"
41 | }
42 | }
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import { Flags, Theme, ThemeError } from "../ThemeTypes";
4 | import { useState, useEffect, useMemo, use } from "react";
5 | import "react-toastify/dist/ReactToastify.css";
6 | import {
7 | checkForNewBackend,
8 | checkIfStandaloneBackendExists,
9 | checkIfBackendIsStandalone,
10 | dummyFunction,
11 | reloadBackend,
12 | startBackend,
13 | recursiveCheck,
14 | getInstalledThemes,
15 | getOS,
16 | generatePresetFromThemeNames,
17 | getLastLoadErrors,
18 | changePreset,
19 | getBackendVersion,
20 | } from "../backend";
21 | import { themeContext } from "@contexts/themeContext";
22 | import { FontContext } from "@contexts/FontContext";
23 | import { backendStatusContext } from "@contexts/backendStatusContext";
24 | import { AppRoot } from "@components/AppRoot";
25 | import DynamicTitleBar from "@components/Native/DynamicTitlebar";
26 | import { AppFrame } from "@components/Native/AppFrame";
27 | import { osContext } from "@contexts/osContext";
28 | import { useBasicAsyncEffect } from "@hooks/useBasicAsyncEffect";
29 |
30 | export default function App(AppProps: AppProps) {
31 | const [themes, setThemes] = useState([]);
32 | const [errors, setErrors] = useState([]);
33 | // This is now undefined before the initial check, that way things can use dummyResult !== undefined to see if the app has properly loaded
34 | const [dummyResult, setDummyResult] = useState(undefined);
35 | const [backendExists, setBackendExists] = useState(false);
36 | const [newBackendVersion, setNewBackend] = useState("");
37 | const [showNewBackendPage, setShowNewBackend] = useState(false);
38 | const [backendManifestVersion, setManifestVersion] = useState(8);
39 | const [OS, setOS] = useState("");
40 | const isWindows = useMemo(() => OS === "win32", [OS]);
41 | const [maximized, setMaximized] = useState(false);
42 | const [fullscreen, setFullscreen] = useState(false);
43 |
44 | const selectedPreset = useMemo(
45 | () => themes.find((e) => e.flags.includes(Flags.isPreset) && e.enabled),
46 | [themes]
47 | );
48 |
49 | useEffect(() => {
50 | let unsubscribeToWindowChanges: () => void;
51 |
52 | async function subscribeToWindowChanges() {
53 | // why did you use a ssr framework in an app
54 | const { appWindow } = await import("@tauri-apps/api/window");
55 | unsubscribeToWindowChanges = await appWindow.onResized(() => {
56 | appWindow.isMaximized().then(setMaximized);
57 | appWindow.isFullscreen().then(setFullscreen);
58 | });
59 | }
60 |
61 | subscribeToWindowChanges();
62 |
63 | // This sets OS and isWindows, which some other initializing logic then runs based on that result
64 | getOS().then(setOS);
65 | // This actually initializes the themes and such
66 | recheckDummy();
67 |
68 | return () => {
69 | unsubscribeToWindowChanges && unsubscribeToWindowChanges();
70 | };
71 | }, []);
72 |
73 | useBasicAsyncEffect(async () => {
74 | if (!isWindows) return;
75 | refreshBackendExists();
76 | const isStandalone = await checkIfBackendIsStandalone();
77 | if (!isStandalone) return;
78 | const newStandalone = await checkForNewBackend();
79 | if (!newStandalone) return;
80 | setNewBackend(newStandalone as string);
81 | setShowNewBackend(true);
82 | }, [isWindows]);
83 |
84 | async function recheckDummy() {
85 | recursiveCheck(
86 | dummyFuncTest,
87 | () => refreshThemes(true),
88 | () => isWindows && startBackend()
89 | );
90 | }
91 |
92 | async function refreshBackendExists() {
93 | if (!isWindows) return;
94 | const backendExists = await checkIfStandaloneBackendExists();
95 | setBackendExists(backendExists);
96 | }
97 |
98 | async function dummyFuncTest() {
99 | try {
100 | const data = await dummyFunction();
101 | if (!data || !data.success) throw new Error(undefined);
102 | setDummyResult(data.result);
103 | return true;
104 | } catch {
105 | setDummyResult(false);
106 | return false;
107 | }
108 | }
109 |
110 | async function refreshThemes(reset: boolean = false): Promise {
111 | if (isWindows) await refreshBackendExists();
112 | await dummyFuncTest();
113 | const backendVer = await getBackendVersion();
114 | if (backendVer.success) {
115 | setManifestVersion(backendVer.result);
116 | }
117 |
118 | const data = reset ? await reloadBackend() : await getInstalledThemes();
119 | if (data) {
120 | setThemes(data.sort());
121 | }
122 | const errors = await getLastLoadErrors();
123 | if (errors) {
124 | setErrors(errors);
125 | }
126 |
127 | // Returning themes for preset thingy thingy
128 | return data?.sort();
129 | }
130 |
131 | return (
132 |
135 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { BiInfoCircle, BiReset } from "react-icons/bi";
2 | import { OneColumnThemeView, TwoColumnThemeView, PresetSelectionDropdown } from "../components";
3 | import { useVW } from "../hooks/useVW";
4 | import { themeContext } from "@contexts/themeContext";
5 | import { useContext, useState, useEffect, useMemo } from "react";
6 | import { LabelledInput, RadioDropdown, Tooltip, TwoItemToggle } from "@components/Primitives";
7 | import { TbColumns1, TbColumns2 } from "react-icons/tb";
8 | import { Flags } from "ThemeTypes";
9 | import Link from "next/link";
10 | import { storeRead, storeWrite } from "backend";
11 | import { test } from "backend/tauriMethods/test";
12 |
13 | export default function MainPage() {
14 | const vw = useVW();
15 | const { refreshThemes, themes } = useContext(themeContext);
16 | const [search, setSearch] = useState("");
17 | const [numCols, setNumCols] = useState(1);
18 | const [sortValue, setSort] = useState("nameAZ");
19 |
20 | useEffect(() => {
21 | storeRead("desktopDisplay").then((res) => {
22 | if (!res.success) return;
23 | if (res.result === "1" || res.result === "2") setNumCols(Number(res.result));
24 | });
25 | storeRead("desktopSort").then((res) => {
26 | if (!res.success || res?.result?.length === 0) return;
27 | setSort(res.result);
28 | });
29 | }, []);
30 |
31 | const [sortedThemes] = useMemo(() => {
32 | const filteredAll = themes.filter(
33 | (e) =>
34 | e.name.toLowerCase().includes(search.toLowerCase()) ||
35 | e.author.toLowerCase().includes(search)
36 | );
37 | const sortedAll = filteredAll.sort((a, b) => {
38 | switch (sortValue) {
39 | case "nameAZ": {
40 | return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
41 | }
42 | case "nameZA": {
43 | return a.name < b.name ? 1 : a.name > b.name ? -1 : 0;
44 | }
45 | case "authorAZ": {
46 | return a.author < b.author ? -1 : a.author > b.author ? 1 : 0;
47 | }
48 | case "authorZA": {
49 | return a.author < b.author ? 1 : a.author > b.author ? -1 : 0;
50 | }
51 | case "created": {
52 | return a.created > b.created ? -1 : a.created < b.created ? 1 : 0;
53 | }
54 | case "modified": {
55 | return a.modified > b.modified ? -1 : a.modified < b.modified ? 1 : 0;
56 | }
57 | default: {
58 | return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
59 | }
60 | }
61 | });
62 | // const sortedPresets = sortedAll.filter((e) => e.flags.includes(Flags.isPreset));
63 | const sortedThemes = sortedAll.filter((e) => !e.flags.includes(Flags.isPreset));
64 | return [sortedThemes];
65 | }, [themes, search, sortValue]);
66 |
67 | useEffect(() => {
68 | if (vw < 650 && numCols === 2) {
69 | setNumCols(1);
70 | storeWrite("desktopDisplay", "1");
71 | }
72 | }, [vw]);
73 |
74 | return (
75 | <>
76 |
77 |
78 | {/*
79 | Installed Themes
80 | */}
81 |
82 |
83 |
84 | {
89 | setSort(value);
90 | storeWrite("desktopSort", value);
91 | }}
92 | options={[
93 | { value: "nameAZ", displayText: "Theme Name (A to Z)" },
94 | { value: "nameZA", displayText: "Theme Name (Z to A)" },
95 | { value: "authorAZ", displayText: "Author Name (A to Z)" },
96 | { value: "authorZA", displayText: "Author Name (Z to A)" },
97 | { value: "created", displayText: "Recently Installed" },
98 | { value: "modified", displayText: "Last Modified" },
99 | ]}
100 | />
101 | },
110 | { value: 2, displayText: },
111 | ]}
112 | onValueChange={(value) => {
113 | setNumCols(value);
114 | storeWrite("desktopDisplay", value + "");
115 | }}
116 | />
117 |
118 |
119 |
120 |
121 |
122 |
Themes
123 |
126 | {themes.length > 0 ? (
127 | <>
128 | {numCols === 1 ? (
129 |
130 | ) : (
131 |
132 | )}
133 | >
134 | ) : (
135 | <>
136 |
137 | You have no themes installed. Download some from{" "}
138 |
139 | the store
140 |
141 |
142 | >
143 | )}
144 |
145 |
146 |
147 |
148 | {
151 | refreshThemes(true);
152 | }}
153 | >
154 |
155 | Refresh Injector
156 |
157 |
158 |
159 | >
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/pages/manage-themes.tsx:
--------------------------------------------------------------------------------
1 | import { themeContext } from "@contexts/themeContext";
2 | import { useContext, useEffect, useState } from "react";
3 | import { Flags, MinimalCSSThemeInfo, Theme } from "../ThemeTypes";
4 | import { deleteTheme, downloadThemeFromUrl, toast } from "../backend";
5 | import { bulkThemeUpdateCheck } from "../logic";
6 | import { ManageThemeCard, YourProfilesList } from "../components";
7 | import { BiFolderOpen } from "react-icons/bi";
8 | import { ThemeErrorsList } from "@components/ManageThemes/ThemeErrorsList";
9 |
10 | export type LocalThemeStatus = "installed" | "outdated" | "local";
11 |
12 | export default function ManageThemes() {
13 | const { themes: localThemeList, refreshThemes } = useContext(themeContext);
14 | const [uninstalling, setUninstalling] = useState(false);
15 | const [updateStatuses, setUpdateStatuses] = useState<
16 | [string, LocalThemeStatus, false | MinimalCSSThemeInfo][]
17 | >([]);
18 |
19 | function handleUninstall(listEntry: Theme) {
20 | setUninstalling(true);
21 | deleteTheme(listEntry.name).then(() => {
22 | refreshThemes(true);
23 | setUninstalling(false);
24 | });
25 | }
26 |
27 | function handleUpdate(remoteEntry: MinimalCSSThemeInfo) {
28 | downloadThemeFromUrl(remoteEntry.id).then(() => {
29 | toast(`${remoteEntry.name} Updated`);
30 | refreshThemes(true);
31 | });
32 | }
33 |
34 | useEffect(() => {
35 | bulkThemeUpdateCheck(localThemeList).then((value) => {
36 | setUpdateStatuses(value);
37 | });
38 | }, [localThemeList]);
39 |
40 | return (
41 |
42 |
43 |
Theme Directory
44 | {
47 | // These have to be async imported here as otherwise NextJS tries to "SSR" them and it errors
48 | const { homeDir, join } = await import("@tauri-apps/api/path");
49 | const { open } = await import("@tauri-apps/api/shell");
50 | const userDir = await homeDir();
51 | const path = await join(userDir, "homebrew", "themes");
52 | open(path);
53 | }}
54 | >
55 |
56 | Open Themes Directory
57 |
58 |
59 |
60 |
61 | Installed Themes
62 |
63 |
64 | {localThemeList
65 | .filter((e) => !e.flags.includes(Flags.isPreset))
66 | .map((e) => (
67 |
74 | ))}
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/pages/store.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useRef } from "react";
2 | import { themeContext } from "@contexts/themeContext";
3 | import { allowedStoreOrigins, storeUrl } from "../constants";
4 | import { downloadThemeFromUrl, sleep, storeRead, toast } from "../backend";
5 | import { useRouter } from "next/router";
6 | import Image from "next/image";
7 |
8 | export default function Store() {
9 | const storeRef = useRef();
10 | const router = useRouter();
11 | const { refreshThemes, themes } = useContext(themeContext);
12 |
13 | useEffect(() => {
14 | function listener(event: any) {
15 | if (!allowedStoreOrigins.includes(event.origin)) return;
16 | if (event.data.action === "installTheme") {
17 | downloadThemeFromUrl(event.data.payload).then(() => {
18 | toast(`Theme Installed`);
19 | refreshThemes(true);
20 | storeRef.current?.contentWindow?.postMessage({ action: "themeInstalled" }, event.origin);
21 | });
22 | }
23 | if (event.data.action === "tokenRedirect") {
24 | router.push("/settings");
25 | }
26 | }
27 | window.addEventListener("message", listener);
28 | return () => {
29 | window.removeEventListener("message", listener);
30 | };
31 | }, []);
32 | return (
33 | <>
34 |
89 | >
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require("prettier-plugin-tailwindcss")],
3 | singleQuote: false,
4 | tabWidth: 2,
5 | semi: true,
6 | trailingComma: "es5",
7 | bracketSameLine: false,
8 | printWidth: 100,
9 | quoteProps: "as-needed",
10 | bracketSpacing: true,
11 | arrowParens: "always",
12 | };
13 |
--------------------------------------------------------------------------------
/public/CSSLoaderWordmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo_css_darkmode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/public/logo_css_darkmode.png
--------------------------------------------------------------------------------
/public/logo_css_lightmode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/public/logo_css_lightmode.png
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | license = ""
7 | repository = ""
8 | default-run = "app"
9 | edition = "2021"
10 | rust-version = "1.59"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "1.2.1", features = [] }
16 |
17 | [dependencies]
18 | zip-extract = "0.1.2"
19 | home = "0.5.4"
20 | serde_json = "1.0"
21 | serde = { version = "1.0", features = ["derive"] }
22 | tauri = { version = "1.2.4", features = ["dialog-all", "fs-create-dir", "fs-exists", "fs-read-file", "fs-write-file", "http-all", "os-all", "path-all", "shell-all", "updater", "window-close", "window-maximize", "window-minimize", "window-set-decorations", "window-set-focus", "window-set-fullscreen", "window-set-size", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
23 | reqwest = { version = "0.11.0", features = ["json"] }
24 | tokio = { version = "1", features = ["full"] }
25 | winapi = { version = "0.3.9", features = ["tlhelp32"] }
26 | log = "0.4.20"
27 | directories = "5.0.1"
28 |
29 | [features]
30 | # by default Tauri runs in production mode
31 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
32 | default = ["custom-protocol"]
33 | # this feature is used for production builds where `devPath` points to the filesystem
34 | # DO NOT remove this
35 | custom-protocol = ["tauri/custom-protocol"]
36 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/101157a8056444013a4879e913f8a6bbbc3e68a8/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 | use std::io::Cursor;
6 | use std::path::{Path, PathBuf};
7 | use directories::BaseDirs;
8 | use home::home_dir;
9 | use zip_extract;
10 | use std::process::Command;
11 | use std::{fs, ptr};
12 |
13 | #[cfg(target_os = "windows")]
14 | use {
15 | winapi::um::tlhelp32::{CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32},
16 | winapi::um::processthreadsapi::{OpenProcess, TerminateProcess},
17 | winapi::um::winnt::{PROCESS_QUERY_INFORMATION, PROCESS_VM_READ},
18 | winapi::um::handleapi::CloseHandle,
19 | winapi::shared::minwindef::DWORD,
20 | };
21 |
22 |
23 | #[cfg(target_os = "windows")]
24 | fn main() {
25 | tauri::Builder::default()
26 | .invoke_handler(tauri::generate_handler![download_template,kill_standalone_backend,download_latest_backend,start_backend,install_backend,get_string_startup_dir])
27 | .run(tauri::generate_context!())
28 | .expect("error while running tauri application");
29 | }
30 |
31 | #[cfg(target_os = "linux")]
32 | fn main() {
33 | tauri::Builder::default()
34 | .invoke_handler(tauri::generate_handler![download_template])
35 | .run(tauri::generate_context!())
36 | .expect("error while running tauri application");
37 | }
38 |
39 | #[tauri::command]
40 | async fn download_template(template_name: String) -> bool {
41 |
42 | let mut home = home_dir().expect("");
43 | if home.join("homebrew/themes").exists() {
44 | home = home.join("homebrew/themes")
45 | }
46 |
47 | let url: String = "https://api.deckthemes.com/themes/template/css?themename=".to_owned() + &template_name;
48 | let client: reqwest::Client = reqwest::Client::new();
49 | let res: reqwest::Response = client.get(url).send().await.expect("");
50 | let bytes = res.bytes().await.expect("");
51 |
52 | let vec: Vec = bytes.to_vec();
53 |
54 | let extract = zip_extract::extract(Cursor::new(vec), &home, false);
55 | return !extract.is_err()
56 | }
57 |
58 | #[cfg(target_os = "windows")]
59 | async fn get_startup_dir() -> Option {
60 | if let Some(base_dirs) = BaseDirs::new() {
61 | let config = base_dirs.config_dir();
62 | let startup_dir: std::path::PathBuf = Path::new(&config).join("Microsoft\\Windows\\Start Menu\\Programs\\Startup");
63 | // TODO: MAKE SURE THE FILE OR DIRECTORY EXISTS
64 | // MAYBE NOT THE FILE AS ON INITIAL INSTALL IT WONT EXIST
65 | // BUT THE FOLDER FOR SURE
66 | return Some(startup_dir);
67 | }
68 | return None;
69 | }
70 |
71 | #[cfg(target_os = "windows")]
72 | async fn get_backend_path() -> Option {
73 | let startup_dir = get_startup_dir().await;
74 | if startup_dir.is_none() {
75 | return None;
76 | }
77 | let backend_file_name = startup_dir.unwrap().join("CssLoader-Standalone-Headless.exe");
78 | return Some(backend_file_name);
79 | }
80 |
81 | #[cfg(target_os = "windows")]
82 | #[tauri::command]
83 | async fn get_string_startup_dir() -> String {
84 | let startup_dir = get_startup_dir().await;
85 | if startup_dir.is_none() {
86 | return "ERROR:".to_owned();
87 | }
88 | return startup_dir.unwrap().to_string_lossy().to_string();
89 | }
90 |
91 | #[cfg(target_os = "windows")]
92 | #[tauri::command]
93 | async fn install_backend(backend_url: String) -> String {
94 | kill_standalone_backend().await;
95 | println!("Backend Killed");
96 | download_latest_backend(backend_url).await;
97 | println!("Backend Downloaded");
98 | start_backend().await;
99 | println!("Backend Started");
100 | return String::from("SUCCESS");
101 | }
102 |
103 | #[cfg(target_os = "windows")]
104 | #[tauri::command]
105 | async fn start_backend() -> String {
106 | let backend_file_name = get_backend_path().await;
107 | if !backend_file_name.is_some() {
108 | return String::from("ERROR: Cannot Find Backend");
109 | }
110 | let file = backend_file_name.unwrap();
111 | println!("Starting New {}", &file.to_string_lossy());
112 | Command::new(&file).spawn().expect("Failed to start the process");
113 | println!("Started");
114 | return String::from("SUCCESS");
115 | }
116 |
117 | #[cfg(target_os = "windows")]
118 | #[tauri::command]
119 | async fn download_latest_backend(backend_url: String) -> String {
120 | let backend_file_name = get_backend_path().await;
121 | if !backend_file_name.is_some() {
122 | return String::from("ERROR: Cannot Find Backend");
123 | }
124 | // Check backend is not running
125 | let process_id: Option> = find_standalone_pids().await;
126 | if process_id.is_some() {
127 | kill_standalone_backend().await;
128 | }
129 |
130 | let client: reqwest::Client = reqwest::Client::new();
131 | let res: reqwest::Response = client.get(backend_url).send().await.expect("");
132 | let bytes = res.bytes().await.expect("");
133 | let vec: Vec = bytes.to_vec();
134 |
135 | println!("Writing File");
136 | let _ = fs::write(backend_file_name.unwrap(), vec);
137 | println!("File written");
138 |
139 | return String::from("SUCCESS");
140 | }
141 |
142 | #[cfg(target_os = "windows")]
143 | async fn find_standalone_pids() -> Option> {
144 |
145 | let process_name: &str = "CssLoader-Standalone-Headless.exe";
146 |
147 | unsafe {
148 | let snapshot_handle = CreateToolhelp32Snapshot(winapi::um::tlhelp32::TH32CS_SNAPPROCESS, 0);
149 |
150 | if snapshot_handle == ptr::null_mut() {
151 | println!("Failed to create snapshot. Error code: {}", winapi::um::errhandlingapi::GetLastError());
152 | return None;
153 | }
154 |
155 | let mut process_entry: PROCESSENTRY32 = std::mem::zeroed();
156 | process_entry.dwSize = std::mem::size_of::() as DWORD;
157 |
158 | if Process32First(snapshot_handle, &mut process_entry) != 0 {
159 | let mut entries: Vec = Vec::new();
160 | loop {
161 | let exe_name = std::ffi::CStr::from_ptr(process_entry.szExeFile.as_ptr() as *const i8).to_string_lossy();
162 |
163 | if exe_name == process_name {
164 | let process_id = process_entry.th32ProcessID;
165 |
166 | let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, process_id);
167 |
168 | if process_handle != ptr::null_mut() {
169 | println!("Found process {} with PID: {}", process_name, process_id);
170 | CloseHandle(process_handle);
171 | entries.push(process_id);
172 | } else {
173 | println!("Failed to open process. Error code: {}", winapi::um::errhandlingapi::GetLastError());
174 | }
175 | }
176 |
177 | if Process32Next(snapshot_handle, &mut process_entry) == 0 {
178 | break;
179 | }
180 | }
181 | if entries.len() == 0 {
182 | return None;
183 | }
184 | return Some(entries);
185 | }
186 |
187 | CloseHandle(snapshot_handle);
188 | return None;
189 | }
190 | }
191 |
192 |
193 | #[cfg(target_os = "windows")]
194 | #[tauri::command]
195 | async fn kill_standalone_backend() -> String {
196 | let process_ids: Option> = find_standalone_pids().await;
197 |
198 | if !process_ids.is_some() {
199 | return String::from("ERROR: No Process Id")
200 | }
201 |
202 | let entries: Vec = process_ids.unwrap();
203 | if entries.len() == 0 {
204 | return String::from("ERROR: Process IDs Length 0");
205 | }
206 |
207 | for id in entries.iter() {
208 |
209 | let res: String = kill_pid(id.to_owned()).await;
210 |
211 | if res.contains("ERROR") {
212 | return format!("ERROR: Error killing process, {}", res);
213 | }
214 | }
215 | return String::from("SUCCESS:");
216 | }
217 |
218 | #[cfg(target_os = "windows")]
219 | async fn kill_pid(process_id: u32) -> String {
220 | unsafe {
221 | // Get a handle to the process
222 | let process_handle = winapi::um::processthreadsapi::OpenProcess(
223 | winapi::um::winnt::PROCESS_TERMINATE,
224 | 0,
225 | process_id,
226 | );
227 |
228 | if process_handle.is_null() {
229 | println!("Failed to open process. Error code: {}", winapi::um::errhandlingapi::GetLastError());
230 | return format!("ERROR: Failed to open process. Error Code {}", winapi::um::errhandlingapi::GetLastError());
231 | }
232 |
233 | // Terminate the process
234 | let result = TerminateProcess(process_handle, 1);
235 |
236 | if result == 0 {
237 | println!("Failed to terminate process. Error code: {}", winapi::um::errhandlingapi::GetLastError());
238 | } else {
239 | println!("Process terminated successfully.");
240 | }
241 |
242 | // Close the process handle
243 | CloseHandle(process_handle);
244 |
245 | return String::from("SUCCESS:");
246 | }
247 | }
248 |
249 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "npm run build && npm run export",
5 | "beforeDevCommand": "npm run dev",
6 | "devPath": "http://localhost:3000",
7 | "distDir": "../out"
8 | },
9 | "package": {
10 | "productName": "CSSLoader Desktop",
11 | "version": "../package.json"
12 | },
13 | "tauri": {
14 | "allowlist": {
15 | "os": {
16 | "all": true
17 | },
18 | "path": {
19 | "all": true
20 | },
21 | "fs": {
22 | "all": false,
23 | "copyFile": false,
24 | "createDir": true,
25 | "exists": true,
26 | "readDir": false,
27 | "readFile": true,
28 | "removeDir": false,
29 | "removeFile": false,
30 | "renameFile": false,
31 | "scope": [
32 | "$CONFIG\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\*",
33 | "$APPDATA/standaloneVersion.txt",
34 | "$APPDATA",
35 | "$HOME/homebrew/themes/*"
36 | ],
37 | "writeFile": true
38 | },
39 | "window": {
40 | "all": false,
41 | "center": false,
42 | "close": true,
43 | "create": false,
44 | "hide": false,
45 | "maximize": true,
46 | "minimize": true,
47 | "print": false,
48 | "requestUserAttention": false,
49 | "setAlwaysOnTop": false,
50 | "setCursorGrab": false,
51 | "setCursorIcon": false,
52 | "setCursorPosition": false,
53 | "setCursorVisible": false,
54 | "setDecorations": true,
55 | "setFocus": true,
56 | "setFullscreen": true,
57 | "setIcon": false,
58 | "setMaxSize": false,
59 | "setMinSize": false,
60 | "setPosition": false,
61 | "setResizable": false,
62 | "setSize": true,
63 | "setSkipTaskbar": false,
64 | "setTitle": false,
65 | "show": false,
66 | "startDragging": true,
67 | "unmaximize": true,
68 | "unminimize": true
69 | },
70 | "shell": {
71 | "all": true,
72 | "execute": false,
73 | "open": false,
74 | "scope": [
75 | {
76 | "name": "killBackend",
77 | "cmd": "powershell.exe",
78 | "args": ["taskkill", "/IM", "CssLoader-Standalone-Headless.exe", "/F"]
79 | },
80 | {
81 | "name": "copyBackend",
82 | "cmd": "powershell.exe",
83 | "args": [
84 | "Copy-Item",
85 | "-Path",
86 | { "validator": ".*" },
87 | "-Destination",
88 | "([Environment]::GetFolderPath('Startup')",
89 | "+",
90 | "'\\CssLoader-Standalone-Headless.exe')"
91 | ]
92 | },
93 | {
94 | "name": "downloadBackend",
95 | "cmd": "powershell.exe",
96 | "args": [
97 | "Invoke-WebRequest",
98 | "-Uri",
99 | "https://github.com/suchmememanyskill/SDH-CssLoader/releases/latest/download/CssLoader-Standalone-Headless.exe",
100 | "-OutFile",
101 | "([Environment]::GetFolderPath('Startup')",
102 | "+",
103 | "'\\CssLoader-Standalone-Headless.exe')"
104 | ]
105 | },
106 | {
107 | "name": "startBackend",
108 | "cmd": "powershell.exe",
109 | "args": [
110 | "Start-Process",
111 | "-FilePath",
112 | "([Environment]::GetFolderPath('Startup')",
113 | "+",
114 | "'\\CssLoader-Standalone-Headless.exe')"
115 | ]
116 | }
117 | ],
118 | "sidecar": false
119 | },
120 | "all": false,
121 | "dialog": {
122 | "all": true,
123 | "ask": true,
124 | "confirm": true,
125 | "message": true,
126 | "open": true,
127 | "save": true
128 | },
129 | "http": {
130 | "all": true,
131 | "request": true,
132 | "scope": [
133 | "http://127.0.0.1:35821/req",
134 | "https://api.deckthemes.com/*",
135 | "https://api.github.com/repos/suchmememanyskill/SDH-CssLoader/*"
136 | ]
137 | }
138 | },
139 | "bundle": {
140 | "active": true,
141 | "category": "DeveloperTool",
142 | "copyright": "",
143 | "deb": {
144 | "depends": []
145 | },
146 | "externalBin": [],
147 | "icon": [
148 | "icons/32x32.png",
149 | "icons/128x128.png",
150 | "icons/128x128@2x.png",
151 | "icons/icon.icns",
152 | "icons/icon.ico"
153 | ],
154 | "identifier": "com.deckthemes.cssloader",
155 | "longDescription": "",
156 | "macOS": {
157 | "entitlements": null,
158 | "exceptionDomain": "",
159 | "frameworks": [],
160 | "providerShortName": null,
161 | "signingIdentity": null
162 | },
163 | "resources": [],
164 | "shortDescription": "",
165 | "targets": ["appimage", "msi", "updater"],
166 | "windows": {
167 | "certificateThumbprint": null,
168 | "digestAlgorithm": "sha256",
169 | "timestampUrl": ""
170 | }
171 | },
172 | "security": {
173 | "csp": null
174 | },
175 | "updater": {
176 | "active": true,
177 | "dialog": true,
178 | "endpoints": [
179 | "https://raw.githubusercontent.com/DeckThemes/CSSLoader-Desktop/main/latest.json"
180 | ],
181 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQ2RUUwREI3QjYzQjlEQzYKUldUR25UdTJ0dzN1MWdPaVZGbGhaWitmNUJKNkNQRUphd0tOdmR6MzNyMHQ3SHorNDcyRTNqS2MK"
182 | },
183 | "windows": [
184 | {
185 | "fullscreen": false,
186 | "height": 600,
187 | "resizable": true,
188 | "title": "CSSLoader Desktop",
189 | "decorations": false,
190 | "transparent": true,
191 | "width": 800,
192 | "minWidth": 460,
193 | "minHeight": 300
194 | }
195 | ]
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | height: 100%;
7 | width: 100%;
8 | }
9 |
10 | html,
11 | body {
12 | /* I'm not sure why you removed the width and height at 100%, but it's super important */
13 | height: 100%;
14 | padding: 0;
15 | margin: 0;
16 | font-family: var(--montserrat), BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
17 | Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
18 | overflow: hidden;
19 | /* background: transparent !important; */
20 | border-radius: 8px;
21 | color-scheme: dark;
22 | }
23 |
24 | /* body {
25 | overflow-y: scroll;
26 | } */
27 |
28 | a {
29 | color: inherit;
30 | text-decoration: none;
31 | }
32 |
33 | * {
34 | box-sizing: border-box;
35 | user-select: none;
36 | outline: none;
37 | font-family: var(--montserrat);
38 | }
39 |
40 | /* *:focus-visible {
41 | box-shadow: 0 0 0 2px transparent, 0 0 0 4px hsl(39 100% 57.0%);
42 | } */
43 |
44 | @media (prefers-color-scheme: dark) {
45 | html {
46 | color-scheme: dark;
47 | }
48 |
49 | body {
50 | color: white;
51 | }
52 | }
53 |
54 | .font-fancy {
55 | font-family: var(--montserrat);
56 | }
57 |
58 | .font-notfancy {
59 | font-family: var(--opensans);
60 | }
61 |
62 | main {
63 | font-family: var(--opensans);
64 | }
65 |
66 | iframe {
67 | background: transparent !important;
68 | }
69 |
70 | input[type="color"] {
71 | -webkit-appearance: none;
72 | border: none;
73 | width: 32px;
74 | height: 32px;
75 | }
76 | input[type="color"]::-webkit-color-swatch-wrapper {
77 | padding: 0;
78 | }
79 | input[type="color"]::-webkit-color-swatch {
80 | border: none;
81 | }
82 |
83 | ::-webkit-scrollbar {
84 | width: 15px;
85 | height: 0px;
86 | background: transparent;
87 | padding-right: 8px;
88 | }
89 |
90 | ::-webkit-scrollbar-corner {
91 | background: transparent;
92 | }
93 |
94 | ::-webkit-scrollbar-thumb {
95 | border-radius: 9999px;
96 | background: #484848;
97 | background-clip: padding-box;
98 | border: 5px solid rgba(0, 0, 0, 0);
99 | border-width: 5px;
100 | }
101 |
102 | ::-webkit-scrollbar-track {
103 | border-radius: 9999px;
104 | background: transparent;
105 | }
106 |
107 | .page-shadow {
108 | box-shadow: 0px 0px 30px hsl(220, 5%, 0%);
109 | }
110 |
111 | .modal-shadow {
112 | box-shadow: 0px 0px 20px hsl(220, 5%, 0%);
113 | }
114 |
115 | @keyframes load-in {
116 | 0% {
117 | /* transform: translateY(24px); */
118 | opacity: 0;
119 | pointer-events: none;
120 | }
121 | 100% {
122 | transform: none;
123 | opacity: 1;
124 | pointer-events: auto;
125 | }
126 | }
127 |
128 | .iframe-load-animation {
129 | opacity: 0;
130 |
131 | animation: load-in 0.4s forwards;
132 | animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
133 | animation-delay: 0.65s;
134 |
135 | will-change: transform;
136 | }
137 |
138 | .cssloader-titlebar {
139 | top: 0;
140 | left: 0;
141 | right: 0;
142 | }
143 |
144 | .radio-dropdown {
145 | box-shadow: 0 0 30px #000;
146 | }
147 |
148 | @keyframes hue-rotate {
149 | 0% {
150 | filter: hue-rotate(0deg) brightness(135%);
151 | }
152 |
153 | 50% {
154 | filter: hue-rotate(360deg) brightness(135%);
155 | }
156 |
157 | 100% {
158 | filter: hue-rotate(0deg) brightness(135%);
159 | }
160 | }
161 |
162 | /* @keyframes store-transform {
163 | 0% {
164 | transform: translateY(100vh) scale(1.5);
165 | opacity: 1;
166 | }
167 |
168 | 100% {
169 | transform: translateY(15vh);
170 | opacity: 0.6;
171 | }
172 | } */
173 |
174 | @keyframes store-transform-skeleton {
175 | 0% {
176 | transform: translateY(-56px);
177 | width: calc(100% - 38px);
178 | }
179 |
180 | 100% {
181 | transform: none;
182 | width: calc(100% - 44px);
183 | }
184 | }
185 |
186 | /* .store-loading-animation {
187 | pointer-events: none;
188 | transition: 250ms;
189 | will-change: transform, filter;
190 | filter: grayscale(1);
191 | animation: store-transform 0.7s both;
192 | animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
193 | animation-delay: 0.25s;
194 | } */
195 |
196 | .store-loading-animation-skeleton {
197 | opacity: 1;
198 | pointer-events: none;
199 | transition: 250ms;
200 | will-change: transform, filter;
201 |
202 | animation: store-transform-skeleton 0.9s both;
203 | animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
204 | /* animation-delay: 0.2s; */
205 | }
206 |
207 | @keyframes pulse {
208 | 50% {
209 | opacity: .5;
210 | transform: scale(0.9)
211 | }
212 | }
213 | .store-loading-img {
214 | animation: pulse 1.2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
215 | }
216 |
217 | .backend-loading-animation {
218 | animation: hue-rotate 4s linear infinite forwards;
219 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: "class",
4 | content: [
5 | "./app/**/*.{js,ts,jsx,tsx}",
6 | "./pages/**/*.{js,ts,jsx,tsx}",
7 | "./components/**/*.{js,ts,jsx,tsx}",
8 |
9 | // Or if using `src` directory:
10 | "./src/**/*.{js,ts,jsx,tsx}",
11 | ],
12 | theme: {
13 | extend: {
14 | height: {
15 | minusNav: "calc(100vh - 4rem)",
16 | },
17 | screens: {
18 | "2cols": "650px",
19 | },
20 | colors: {
21 | base: {
22 | 1: {
23 | dark: "hsl(220, 11%, 5.5%)",
24 | light: "hsl(264, 6%, 98.5%)",
25 | },
26 | 2: {
27 | dark: "hsla(220, 12%, 6%, 0.8)",
28 | light: "hsl(264, 7%, 100%)",
29 | },
30 | "2T": {
31 | dark: "hsla(220, 12%, 8%, 0.7)",
32 | light: "hsla(264, 100%, 100%, 0.7)",
33 | },
34 | "3T": {
35 | dark: "hsla(220, 9%, 60%, 0.1)",
36 | light: "hsla(264, 100%, 100%, 0.7)",
37 | },
38 | 3: {
39 | dark: "hsl(220, 10%, 13%)",
40 | light: "hsl(264, 100%, 100%)",
41 | },
42 | 4: {
43 | dark: "hsl(220, 11%, 15%)",
44 | light: "hsl(264, 1%, 88%)",
45 | },
46 | "4T": {
47 | dark: "hsla(220, 9%, 30%, 0.33)",
48 | light: "hsla(264, 20%, 10%, 0.12)",
49 | },
50 | 5: {
51 | dark: "hsl(220, 11%, 12%)",
52 | light: "hsl(264, 3%, 90%)",
53 | },
54 | 5.5: {
55 | dark: "#151619",
56 | },
57 | "5T": {
58 | dark: "hsla(220, 13%, 60%, 0.15)",
59 | light: "hsla(264, 15%, 10%, 0.09)",
60 | },
61 | 6: {
62 | dark: "hsl(220, 13%, 4%)",
63 | light: "hsl(264, 3%, 95%)",
64 | },
65 | "6T": {
66 | dark: "hsla(220, 13%, 4%, 0.7)",
67 | light: "hsla(264, 10%, 14%, 0.05)",
68 | },
69 | contrast: {
70 | dark: "hsl(270, 3%, 75%)",
71 | light: "hsl(264, 6%, 17%)",
72 | },
73 | },
74 | fore: {
75 | 11: {
76 | dark: "hsl(220, 3%, 95%)",
77 | light: "hsl(264, 6%, 8%)",
78 | },
79 | 10: {
80 | dark: "hsl(220, 3%, 75%)",
81 | light: "hsl(264, 6%, 17%)",
82 | },
83 | 9: {
84 | dark: "hsl(220, 3%, 69%)",
85 | light: "hsl(264, 6%, 30%)",
86 | },
87 | "9Hex": {
88 | dark: "#aeafb2",
89 | light: "#4c4851",
90 | },
91 | 2: {
92 | dark: "hsla(220, 20%, 83%, 0.15)",
93 | light: "hsla(264, 24%, 10%, 0.08)",
94 | },
95 | 3: {
96 | dark: "hsl(220, 3%, 20%)",
97 | light: "hsl(264, 3%, 86%)",
98 | },
99 | 4: {
100 | dark: "hsl(220, 3%, 20%)",
101 | light: "hsl(264, 3%, 79%)",
102 | },
103 | "3T": {
104 | dark: "hsla(220, 20%, 83%, 0.19)",
105 | light: "hsla(264, 24%, 10%, 0.14)",
106 | },
107 | "4T": {
108 | dark: "hsla(220, 26%, 89%, 0.31)",
109 | light: "hsla(264, 28%, 10%, 0.26)",
110 | },
111 | "5T": {
112 | dark: "hsla(220, 26%, 89%, 0.31)",
113 | light: "hsla(264, 28%, 10%, 0.34)",
114 | },
115 | 6: {
116 | dark: "hsl(220, 3%, 45%)",
117 | light: "hsl(264, 3%, 59%)",
118 | },
119 | 5: {
120 | dark: "hsl(220, 3%, 45%)",
121 | light: "hsl(264, 3%, 69%)",
122 | },
123 | "6T": {
124 | dark: "hsla(220, 3%, 80%, 0.5)",
125 | light: "hsla(264, 3%, 17%, 0.5)",
126 | },
127 | 8: {
128 | dark: "hsl(220, 3%, 55%)",
129 | light: "hsl(264, 3%, 42%)",
130 | },
131 | contrast: {
132 | dark: "hsl(220, 11%, 10%)",
133 | light: "hsl(115, 100%, 100%)",
134 | },
135 | },
136 | borders: {
137 | base1: {
138 | dark: "hsl(220, 9%, 14%)",
139 | light: "hsl(220, 9%, 84%)",
140 | },
141 | base2: {
142 | dark: "hsl(220, 9%, 20%)",
143 | light: "hsl(220, 9%, 65%)",
144 | },
145 | base3: {
146 | dark: "hsl(220, 9%, 28%)",
147 | light: "hsl(220, 9%, 90%)",
148 | },
149 | },
150 | shadows: {
151 | menuShadow: {
152 | dark: "0px 8px 25px hsla(220, 5%, 0%, 0.45)",
153 | light: "0px 8px 25px hsla(220, 5%, 0%, 0.45)",
154 | },
155 | modalShadow: {
156 | dark: "0px 16px 34px hsla(220, 4%, 0%, 0.65)",
157 | light: "0px 16px 34px hsla(220, 4%, 50%, 0.5)",
158 | },
159 | sidebarShadow: {
160 | dark: "0px 0px 30px hsl(220, 5%, 0%)",
161 | light: "0px 0px 30px hsl(220, 5%, 0%)",
162 | },
163 | },
164 | "app-neutralDrop": {
165 | dark: "hsla(220, 6%, 9%, 0.9)",
166 | light: "hsla(264, 3%, 92%, 0.9)",
167 | },
168 | "app-backdrop": {
169 | dark: "hsla(220, 5%, 6%, 0.4)",
170 | light: "hsla(264, 3%, 92%, 0.3)",
171 | },
172 | "app-backdropUmbra": {
173 | dark: "hsla(220, 7%, 4%, 0.85)",
174 | light: "hsla(264, 3%, 92%, 0.85)",
175 | },
176 | "app-backdropUmbraSolid": {
177 | dark: "hsl(229, 5%, 4%)",
178 | light: "hsla(264, 3%, 94%, 1)",
179 | },
180 | amber9: "hsl(39 100% 57.0%)",
181 | brandBlue: "#2563eb",
182 | brandBlueTransparent: "#2563eb22",
183 | dangerRed: "#EB3431",
184 | cssPurple: "#de2cf7",
185 | audioBlue: "rgb(26,159,255)",
186 | discordColor: "#5865F2",
187 | patreonColor: "#FF424D",
188 | // These are used to "transform" the card values to look like the bg values
189 | lightenerDark: "rgba(255,255,255,0.1)",
190 | lightenerLight: "rgba(255,255,255,0.3)",
191 | header: {
192 | dark: "hsl(220, 11%, 5.5%)",
193 | light: "",
194 | },
195 | bgDark: "#2e2e2e",
196 | bgLight: "#e2e2e2",
197 | cardDark: "#0000004e",
198 | cardLight: "#0000002e",
199 | elevation: {
200 | 1: {
201 | light: "#0000001e",
202 | dark: "#0000002e",
203 | },
204 | 2: {
205 | light: "#0000002e",
206 | dark: "#0000004e",
207 | },
208 | 3: {
209 | light: "#0000006e",
210 | dark: "#0000006e",
211 | },
212 | },
213 | borderDark: "#0e0e0e",
214 | borderLight: "#a2a2a2",
215 | darkBorderDark: "#020202",
216 | darkBorderLight: "rgb(140,140,140)",
217 | textLight: "#000",
218 | textFadedLight: "#333",
219 | textDark: "#fff",
220 | textFadedDark: "#aaa",
221 | },
222 | },
223 | },
224 | plugins: [],
225 | };
226 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@components/*": ["components/*"],
20 | "@styles/*": ["styles/*"],
21 | "@pages/*": ["pages/*"],
22 | "@hooks/*": ["hooks/*"],
23 | "@contexts/*": ["contexts/*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------