├── src
├── backend
│ ├── backendHelpers
│ │ ├── index.ts
│ │ └── toggleTheme.tsx
│ ├── pythonRoot.ts
│ └── pythonMethods
│ │ ├── storeUtils.ts
│ │ └── pluginSettingsMethods.ts
├── hooks
│ ├── index.ts
│ └── useRerender.ts
├── state
│ └── index.ts
├── components
│ ├── Modals
│ │ ├── ThemeSettingsModal
│ │ │ ├── index.ts
│ │ │ ├── ThemeSettingsModalButtons.tsx
│ │ │ └── ThemeSettingsModal.tsx
│ │ ├── index.ts
│ │ ├── DeleteConfirmationModal.tsx
│ │ ├── CreatePresetModal.tsx
│ │ └── AuthorViewModal.tsx
│ ├── Styles
│ │ ├── index.ts
│ │ ├── ThemeBrowserCardStyles.tsx
│ │ └── ExpandedViewStyles.tsx
│ ├── QAMTab
│ │ ├── index.ts
│ │ ├── QAMThemeToggleList.tsx
│ │ ├── PresetSelectionDropdown.tsx
│ │ └── MOTDDisplay.tsx
│ ├── ThemeManager
│ │ ├── index.ts
│ │ ├── FilterDropdownCustomLabel.tsx
│ │ ├── LoadMoreButton.tsx
│ │ ├── BrowserItemCard.tsx
│ │ └── BrowserSearchFields.tsx
│ ├── index.ts
│ ├── ThemeErrorCard.tsx
│ ├── ThemeSettings
│ │ ├── UpdateAllThemesButton.tsx
│ │ ├── DeleteMenu.tsx
│ │ ├── FullscreenProfileEntry.tsx
│ │ └── FullscreenSingleThemeEntry.tsx
│ ├── DepsOptionSelector.tsx
│ ├── SupporterIcon.tsx
│ ├── OptionalDepsModal.tsx
│ ├── TitleView.tsx
│ ├── ThemePatch.tsx
│ ├── PatchComponent.tsx
│ └── ThemeToggle.tsx
├── logic
│ ├── index.ts
│ ├── calcButtonColor.ts
│ ├── generateParamStr.ts
│ ├── numbers.ts
│ └── bulkThemeUpdateCheck.ts
├── apiTypes
│ ├── index.ts
│ ├── Motd.ts
│ ├── BlobTypes.ts
│ ├── AccountData.ts
│ └── CSSThemeTypes.ts
├── pages
│ ├── theme-manager
│ │ ├── index.ts
│ │ ├── ThemeManagerRouter.tsx
│ │ ├── StarredThemesPage.tsx
│ │ ├── LogInPage.tsx
│ │ ├── SubmissionBrowserPage.tsx
│ │ └── ThemeBrowserPage.tsx
│ └── settings
│ │ ├── Credits.tsx
│ │ ├── PresetSettings.tsx
│ │ ├── SettingsPageRouter.tsx
│ │ ├── PluginSettings.tsx
│ │ ├── ThemeSettings.tsx
│ │ └── DonatePage.tsx
├── deckyPatches
│ ├── NavControllerFinder.tsx
│ ├── SteamTabElementsFinder.tsx
│ ├── NavPatchInfoModal.tsx
│ ├── ClassHashMap.tsx
│ ├── NavPatch.tsx
│ └── UnminifyMode.tsx
├── ThemeTypes.ts
├── styles
│ └── themeSettingsModalStyles.css
├── index.tsx
└── api.ts
├── assets
├── css-store-preview.png
└── paint-roller-solid.png
├── requirements.txt
├── README.md
├── .prettierrc
├── plugin.json
├── .gitignore
├── tsconfig.json
├── css_sfp_compat.py
├── css_server.py
├── rollup.config.js
├── package.json
├── .github
└── workflows
│ └── push.yml
├── css_win_tray.py
├── css_remoteinstall.py
├── css_themepatch.py
├── css_utils.py
├── css_themepatchcomponent.py
├── css_inject.py
└── css_theme.py
/src/backend/backendHelpers/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useRerender";
2 |
--------------------------------------------------------------------------------
/src/state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./CssLoaderState";
2 |
--------------------------------------------------------------------------------
/src/components/Modals/ThemeSettingsModal/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ThemeSettingsModal";
2 |
--------------------------------------------------------------------------------
/src/logic/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./calcButtonColor";
2 | export * from "./generateParamStr";
3 |
--------------------------------------------------------------------------------
/src/components/Modals/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./CreatePresetModal";
2 | export * from "./ThemeSettingsModal";
3 |
--------------------------------------------------------------------------------
/assets/css-store-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/SDH-CssLoader/HEAD/assets/css-store-preview.png
--------------------------------------------------------------------------------
/assets/paint-roller-solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeckThemes/SDH-CssLoader/HEAD/assets/paint-roller-solid.png
--------------------------------------------------------------------------------
/src/components/Styles/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ExpandedViewStyles";
2 | export * from "./ThemeBrowserCardStyles";
3 |
--------------------------------------------------------------------------------
/src/apiTypes/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./CSSThemeTypes";
2 | export * from "./AccountData";
3 | export * from "./BlobTypes";
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.8.1
2 | aiohttp-jinja2==1.5.0
3 | aiohttp_cors==0.7.0
4 | watchdog==2.1.7
5 | certifi==2022.12.7
6 | pystray
--------------------------------------------------------------------------------
/src/backend/pythonRoot.ts:
--------------------------------------------------------------------------------
1 | import { server } from "../python";
2 | import { globalState } from "../python";
3 |
4 | export { server, globalState };
5 |
--------------------------------------------------------------------------------
/src/components/QAMTab/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./QAMThemeToggleList";
2 | export * from "./PresetSelectionDropdown";
3 | export * from "./MOTDDisplay";
4 |
--------------------------------------------------------------------------------
/src/apiTypes/Motd.ts:
--------------------------------------------------------------------------------
1 | export interface Motd {
2 | id: string;
3 | name: string;
4 | description: string;
5 | date: string;
6 | severity: "High" | "Medium" | "Low";
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSS Loader
2 | Dynamically loads CSS files from storage and reloads alongside Steam UI.
3 |
4 | Read the documentation for this tool at [docs.deckthemes.com](https://docs.deckthemes.com/)
--------------------------------------------------------------------------------
/src/apiTypes/BlobTypes.ts:
--------------------------------------------------------------------------------
1 | export type BlobType = "Zip" | "Jpg";
2 |
3 | export interface APIBlob {
4 | id: string;
5 | blobType: BlobType;
6 | uploaded: Date;
7 | downloadCount: number;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/ThemeManager/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./LoadMoreButton";
2 | export * from "./FilterDropdownCustomLabel";
3 | export * from "./BrowserItemCard";
4 | export * from "./BrowserSearchFields";
5 |
--------------------------------------------------------------------------------
/src/pages/theme-manager/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ThemeBrowserPage";
2 | export * from "./ExpandedView";
3 | export * from "./LogInPage";
4 | export * from "./StarredThemesPage";
5 | export * from "./SubmissionBrowserPage";
6 | export * from "./ThemeManagerRouter";
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "tabWidth": 2,
4 | "semi": true,
5 | "trailingComma": "es5",
6 | "bracketSameLine": false,
7 | "printWidth": 100,
8 | "quoteProps": "as-needed",
9 | "bracketSpacing": true,
10 | "arrowParens": "always"
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ThemeToggle";
2 | export * from "./ThemePatch";
3 | export * from "./PatchComponent";
4 | export * from "./TitleView";
5 | export * from "./ThemeManager";
6 | export * from "./OptionalDepsModal";
7 | export * from "./QAMTab";
8 | export * from "./Modals";
9 |
--------------------------------------------------------------------------------
/src/logic/calcButtonColor.ts:
--------------------------------------------------------------------------------
1 | export function calcButtonColor(installStatus: string) {
2 | let filterCSS = "";
3 | switch (installStatus) {
4 | case "outdated":
5 | filterCSS = "invert(6%) sepia(90%) saturate(200%) hue-rotate(160deg) contrast(122%)";
6 | break;
7 | default:
8 | filterCSS = "";
9 | break;
10 | }
11 | return filterCSS;
12 | }
13 |
--------------------------------------------------------------------------------
/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CSS Loader",
3 | "author": "DeckThemes",
4 | "flags": [],
5 | "publish": {
6 | "tags": ["style"],
7 | "description": "Dynamically loads themes developed with CSS into the Steam UI. For more information, visit deckthemes.com.",
8 | "image": "https://raw.githubusercontent.com/suchmememanyskill/SDH-CssLoader/main/assets/css-store-preview.png"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/deckyPatches/NavControllerFinder.tsx:
--------------------------------------------------------------------------------
1 | import { Module, findModuleChild } from "decky-frontend-lib";
2 |
3 | export const NavController = findModuleChild((m: Module) => {
4 | if (typeof m !== "object") return undefined;
5 |
6 | // Pre Chromium-109
7 | if (m?.CFocusNavNode) {
8 | return m.CFocusNavNode;
9 | }
10 |
11 | for (let prop in m) {
12 | if (m[prop]?.prototype?.FindNextFocusableChildInDirection) {
13 | return m[prop];
14 | }
15 | }
16 |
17 | return undefined;
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/ThemeManager/FilterDropdownCustomLabel.tsx:
--------------------------------------------------------------------------------
1 | export function FilterDropdownCustomLabel({
2 | filterValue,
3 | itemCount,
4 | }: {
5 | filterValue: string;
6 | itemCount: number | string;
7 | }) {
8 | return (
9 |
16 | {filterValue}
17 | {itemCount}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 | *.swp
10 |
11 | pids
12 | logs
13 | results
14 | tmp
15 |
16 | # Coverage reports
17 | coverage
18 |
19 | # API keys and secrets
20 | .env
21 |
22 | # Dependency directory
23 | node_modules
24 | bower_components
25 |
26 | # Editors
27 | .idea
28 | *.iml
29 |
30 | # OS metadata
31 | .DS_Store
32 | Thumbs.db
33 |
34 | # Ignore built ts files
35 | dist/
36 |
37 | __pycache__/
38 |
39 | /.yalc
40 | yalc.lock
41 |
42 | .vscode/settings.json
43 |
--------------------------------------------------------------------------------
/src/hooks/useRerender.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | // This should only be used to fix weird bugs with how valve's toggles/dropdowns/etc don't update state
4 | // If used, state why in a comment next to the invocation
5 | export function useRerender(): [boolean, () => void] {
6 | const [render, setRender] = useState(true);
7 | useEffect(() => {
8 | if (render === false) {
9 | setTimeout(() => setRender(true), 100);
10 | }
11 | }, [render]);
12 | const rerender = () => {
13 | setRender(false);
14 | };
15 |
16 | return [render, rerender];
17 | }
18 |
--------------------------------------------------------------------------------
/src/logic/generateParamStr.ts:
--------------------------------------------------------------------------------
1 | export function generateParamStr(origSearchOpts: any, filterPrepend: string = "") {
2 | // This can be done with 'new URLSearchParams(obj)' but I want more control
3 | const searchOpts = Object.assign({}, origSearchOpts);
4 | if (filterPrepend) {
5 | searchOpts.filters = filterPrepend + searchOpts.filters;
6 | }
7 | let paramString = "?";
8 | Object.keys(searchOpts).forEach((key, i) => {
9 | // @ts-ignore typescript doesn't know how object.keys works 🙄
10 | if (searchOpts[key]) {
11 | // @ts-ignore
12 | paramString += `${i !== 0 ? "&" : ""}${key}=${searchOpts[key]}`;
13 | }
14 | });
15 | return paramString;
16 | }
17 |
--------------------------------------------------------------------------------
/src/deckyPatches/SteamTabElementsFinder.tsx:
--------------------------------------------------------------------------------
1 | import { getGamepadNavigationTrees } from "decky-frontend-lib";
2 |
3 | export function getElementFromNavID(navID: string) {
4 | const all = getGamepadNavigationTrees();
5 | if (!all) return null;
6 | const tree = all?.find((e: any) => e.m_ID == navID);
7 | if (!tree) return null;
8 | return tree.Root.Element;
9 | }
10 | export function getSP() {
11 | return getElementFromNavID("root_1_");
12 | }
13 | export function getQAM() {
14 | return getElementFromNavID("QuickAccess-NA");
15 | }
16 | export function getMainMenu() {
17 | return getElementFromNavID("MainNavMenuContainer");
18 | }
19 | export function getRootElements() {
20 | return [getSP(), getQAM(), getMainMenu()].filter((e) => e);
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "ESNext",
5 | "target": "ES2020",
6 | "jsx": "react",
7 | "jsxFactory": "window.SP_REACT.createElement",
8 | "jsxFragmentFactory": "window.SP_REACT.Fragment",
9 | "declaration": false,
10 | "moduleResolution": "node",
11 | "noUnusedLocals": false,
12 | "noUnusedParameters": false,
13 | "esModuleInterop": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strict": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "allowSyntheticDefaultImports": true,
20 | "skipLibCheck": true
21 | },
22 | "include": ["src"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/ThemeErrorCard.tsx:
--------------------------------------------------------------------------------
1 | import { Focusable, PanelSectionRow } from "decky-frontend-lib";
2 | import { ThemeError } from "../ThemeTypes";
3 |
4 | export function ThemeErrorCard({ errorData }: { errorData: ThemeError }) {
5 | return (
6 | {}}
9 | style={{
10 | width: "100%",
11 | margin: 0,
12 | padding: 0,
13 | }}
14 | >
15 |
23 |
24 | {errorData[0]}
25 |
26 | {errorData[1]}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/css_sfp_compat.py:
--------------------------------------------------------------------------------
1 | import os
2 | from css_inject import Inject, to_inject
3 |
4 | SFP_DEFAULT_FILES = {
5 | "libraryroot.custom.css": ["desktop", "desktopoverlay", "desktopcontextmenu"],
6 | "bigpicture.custom.css": ["bigpicture", "bigpictureoverlay"],
7 | "friends.custom.css": ["desktopchat"],
8 | "webkit.css": ["store"]
9 | }
10 |
11 | def is_folder_sfp_theme(dir : str) -> bool:
12 | for x in SFP_DEFAULT_FILES:
13 | if os.path.exists(os.path.join(dir, x)):
14 | return True
15 |
16 | return False
17 |
18 | def convert_to_css_theme(dir : str, theme) -> None:
19 | theme.name = os.path.basename(dir)
20 | theme.id = theme.name
21 | theme.version = "v1.0"
22 | theme.author = ""
23 | theme.require = 1
24 | theme.dependencies = []
25 | theme.injects = [to_inject(x, SFP_DEFAULT_FILES[x], dir, theme) for x in SFP_DEFAULT_FILES if os.path.exists(os.path.join(dir, x))]
--------------------------------------------------------------------------------
/src/apiTypes/AccountData.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from "react";
2 | import { PartialCSSThemeInfo, UserInfo } from "./CSSThemeTypes";
3 |
4 | export enum Permissions {
5 | "editAny" = "EditAnyPost",
6 | "approveSubs" = "ApproveThemeSubmissions",
7 | "viewSubs" = "ViewThemeSubmissions",
8 | "admin" = "ManageApi",
9 | }
10 |
11 | export interface AccountData extends UserInfo {
12 | permissions: Permissions[];
13 | }
14 |
15 | export interface AuthContextContents {
16 | accountInfo: AccountData | undefined;
17 | setAccountInfo:
18 | | Dispatch>
19 | | ((info: AccountData | undefined) => void);
20 | }
21 |
22 | export interface StarContextContents {
23 | starredThemes: StarredThemeList | undefined;
24 | setStarredThemes:
25 | | Dispatch>
26 | | ((info: StarredThemeList | undefined) => void);
27 | }
28 |
29 | export interface StarredThemeList {
30 | total: number;
31 | items: PartialCSSThemeInfo[];
32 | }
33 |
--------------------------------------------------------------------------------
/src/deckyPatches/NavPatchInfoModal.tsx:
--------------------------------------------------------------------------------
1 | import { DialogButton, Focusable, ConfirmModal } from "decky-frontend-lib";
2 | import { Theme } from "../ThemeTypes";
3 | import { setNavPatch } from "./NavPatch";
4 | export function NavPatchInfoModalRoot({
5 | themeData,
6 | closeModal,
7 | }: {
8 | themeData: Theme;
9 | closeModal?: any;
10 | }) {
11 | function onButtonClick() {
12 | setNavPatch(true, true);
13 | closeModal();
14 | }
15 | return (
16 |
24 |
25 | {themeData.name} hides elements that can be selected using a controller. For this to work
26 | correctly, CSS Loader needs to patch controller navigation. Not enabling this feature will
27 | cause visually hidden elements to be able to be selected using a controller.
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/ThemeSettings/UpdateAllThemesButton.tsx:
--------------------------------------------------------------------------------
1 | import { DialogButton } from "decky-frontend-lib";
2 | import { useCssLoaderState } from "../../state";
3 | import { Theme } from "../../ThemeTypes";
4 | import { FaDownload } from "react-icons/fa";
5 |
6 | export function UpdateAllThemesButton({
7 | handleUpdate,
8 | }: {
9 | handleUpdate: (entry: Theme) => Promise;
10 | }) {
11 | const { updateStatuses, localThemeList } = useCssLoaderState();
12 |
13 | async function updateAll() {
14 | const themesToBeUpdated = updateStatuses.filter((e) => e[1] === "outdated");
15 | for (let i = 0; i < themesToBeUpdated.length; i++) {
16 | const entry = localThemeList.find((f) => f.id === themesToBeUpdated[i][0]);
17 | if (!entry) break;
18 | await handleUpdate(entry);
19 | }
20 | }
21 | return (
22 | <>
23 | {updateStatuses.filter((e) => e[1] === "outdated").length > 0 && (
24 |
25 |
26 | Update All Themes
27 |
28 | )}
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/logic/numbers.ts:
--------------------------------------------------------------------------------
1 | // Code from the short-number package, could not be imported due
2 | // to TypeScript issues.
3 | // https://www.npmjs.com/package/short-number
4 |
5 | export function shortenNumber(num: number) {
6 | if (typeof num !== "number") {
7 | throw new TypeError("Expected a number");
8 | }
9 |
10 | if (num > 1e19) {
11 | throw new RangeError("Input expected to be < 1e19");
12 | }
13 |
14 | if (num < -1e19) {
15 | throw new RangeError("Input expected to be > 1e19");
16 | }
17 |
18 | if (Math.abs(num) < 1000) {
19 | return num;
20 | }
21 |
22 | var shortNumber;
23 | var exponent;
24 | var size;
25 | var sign = num < 0 ? "-" : "";
26 | var suffixes = {
27 | K: 6,
28 | M: 9,
29 | B: 12,
30 | T: 16,
31 | };
32 |
33 | num = Math.abs(num);
34 | size = Math.floor(num).toString().length;
35 |
36 | exponent = size % 3 === 0 ? size - 3 : size - (size % 3);
37 | shortNumber = String(Math.round(10 * (num / Math.pow(10, exponent))) / 10);
38 |
39 | for (var suffix in suffixes) {
40 | if (exponent < suffixes[suffix]) {
41 | shortNumber += suffix;
42 | break;
43 | }
44 | }
45 |
46 | return sign + shortNumber;
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/DepsOptionSelector.tsx:
--------------------------------------------------------------------------------
1 | import { DialogButton, Focusable } from "decky-frontend-lib";
2 | import * as python from "../python";
3 |
4 | export function DepsOptionSelector({
5 | themeName,
6 | closeModal = undefined,
7 | }: {
8 | themeName: string;
9 | closeModal?: any;
10 | }) {
11 | function enableTheme(enableDeps: boolean = true, enableDepValues: boolean = true) {
12 | python.resolve(python.setThemeState(themeName, true, enableDeps, enableDepValues), () => {
13 | python.getInstalledThemes();
14 | closeModal && closeModal();
15 | });
16 | }
17 | return (
18 |
19 | enableTheme(true, true)} style={{ margin: "0 10px" }}>
20 | Enable with configuration {"(Recommended)"}
21 |
22 | enableTheme(true, false)} style={{ margin: "0 10px" }}>
23 | Enable without configuration
24 |
25 | enableTheme(false, false)} style={{ margin: "0 10px" }}>
26 | Enable only this theme
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/css_server.py:
--------------------------------------------------------------------------------
1 | import asyncio, aiohttp.web, json
2 | from css_utils import Log, create_cef_flag
3 |
4 | PLUGIN_CLASS = None
5 |
6 | async def handle(request : aiohttp.web.BaseRequest):
7 | data = await request.json()
8 | request_result = {"res": None, "success": True}
9 |
10 | # This is very cool decky code
11 | try:
12 | request_result["res"] = await getattr(PLUGIN_CLASS, data["method"])(PLUGIN_CLASS, **data["args"])
13 | except Exception as e:
14 | request_result["res"] = str(e)
15 | request_result["success"] = False
16 | finally:
17 | return aiohttp.web.Response(text=json.dumps(request_result, ensure_ascii=False), content_type='application/json')
18 |
19 | def start_server(plugin):
20 | global PLUGIN_CLASS
21 |
22 | PLUGIN_CLASS = plugin
23 | loop = asyncio.get_running_loop()
24 |
25 | try:
26 | create_cef_flag()
27 | except Exception as e:
28 | Log(f"Failed to create steam cef flag. {str(e)}")
29 |
30 | app = aiohttp.web.Application(loop=loop)
31 | app.router.add_route('POST', '/req', handle)
32 | loop.create_task(aiohttp.web._run_app(app, host="127.0.0.1", port=35821))
33 | Log("Started CSS_Loader server on port 35821")
--------------------------------------------------------------------------------
/src/ThemeTypes.ts:
--------------------------------------------------------------------------------
1 | import { MinimalCSSThemeInfo } from "./apiTypes";
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 | }
16 |
17 | export interface ThemePatchComponent {
18 | name: string;
19 | on: string;
20 | type: string;
21 | value: string;
22 | }
23 |
24 | export interface Patch {
25 | default: string;
26 | name: string;
27 | type: "dropdown" | "checkbox" | "slider" | "none";
28 | value: string;
29 | options: string[];
30 | components: ThemePatchComponent[];
31 | }
32 |
33 | export enum Flags {
34 | "isPreset" = "PRESET",
35 | "dontDisableDeps" = "KEEP_DEPENDENCIES",
36 | "optionalDeps" = "OPTIONAL_DEPENDENCIES",
37 | "navPatch" = "REQUIRE_NAV_PATCH",
38 | }
39 |
40 | export type LocalThemeStatus = "installed" | "outdated" | "local";
41 | export type UpdateStatus = [string, LocalThemeStatus, false | MinimalCSSThemeInfo];
42 |
43 | type ThemeErrorTitle = string;
44 | type ThemeErrorDescription = string;
45 | export type ThemeError = [ThemeErrorTitle, ThemeErrorDescription];
46 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from "@rollup/plugin-commonjs";
2 | import json from "@rollup/plugin-json";
3 | import { nodeResolve } from "@rollup/plugin-node-resolve";
4 | import replace from "@rollup/plugin-replace";
5 | import typescript from "@rollup/plugin-typescript";
6 | import { defineConfig } from "rollup";
7 | import importAssets from "rollup-plugin-import-assets";
8 | import styles from "rollup-plugin-styles";
9 |
10 | import { name } from "./plugin.json";
11 |
12 | export default defineConfig({
13 | input: "./src/index.tsx",
14 | plugins: [
15 | commonjs(),
16 | nodeResolve(),
17 | typescript(),
18 | json(),
19 | styles(),
20 | replace({
21 | preventAssignment: false,
22 | "process.env.NODE_ENV": JSON.stringify("production"),
23 | }),
24 | importAssets({
25 | publicPath: `http://127.0.0.1:1337/plugins/${name}/`,
26 | }),
27 | ],
28 | context: "window",
29 | external: ["react", "react-dom", "decky-frontend-lib"],
30 | output: {
31 | file: "dist/index.js",
32 | globals: {
33 | react: "SP_REACT",
34 | "react-dom": "SP_REACTDOM",
35 | "decky-frontend-lib": "DFL",
36 | },
37 | format: "iife",
38 | exports: "default",
39 | assetFileNames: "[name]-[hash][extname]",
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/src/backend/pythonMethods/storeUtils.ts:
--------------------------------------------------------------------------------
1 | import { server, toast } from "../../python";
2 |
3 | export async function booleanStoreRead(key: string) {
4 | const deckyRes = await server!.callPluginMethod<{ key: string }, string>("store_read", {
5 | key,
6 | });
7 | if (!deckyRes.success) {
8 | toast(`Error fetching ${key}`, deckyRes.result);
9 | return false;
10 | }
11 | return deckyRes.result === "1" || deckyRes.result === "true";
12 | }
13 |
14 | export async function booleanStoreWrite(key: string, value: boolean) {
15 | const deckyRes = await server!.callPluginMethod<{ key: string; val: string }>("store_write", {
16 | key,
17 | val: value ? "1" : "0",
18 | });
19 | if (!deckyRes.success) {
20 | toast(`Error setting ${key}`, deckyRes.result);
21 | }
22 | }
23 |
24 | export async function stringStoreRead(key: string) {
25 | const deckyRes = await server!.callPluginMethod<{ key: string }, string>("store_read", {
26 | key,
27 | });
28 | if (!deckyRes.success) {
29 | toast(`Error fetching ${key}`, deckyRes.result);
30 | return "";
31 | }
32 | return deckyRes.result;
33 | }
34 | export async function stringStoreWrite(key: string, value: string) {
35 | const deckyRes = await server!.callPluginMethod<{ key: string; val: string }>("store_write", {
36 | key,
37 | val: value,
38 | });
39 | if (!deckyRes.success) {
40 | toast(`Error setting ${key}`, deckyRes.result);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/settings/Credits.tsx:
--------------------------------------------------------------------------------
1 | export function Credits() {
2 | return (
3 |
4 |
5 |
6 |
Developers
7 |
8 | -
9 | SuchMeme - github.com/suchmememanyskill
10 |
11 | -
12 | Beebles - github.com/beebls
13 |
14 | -
15 | EMERALD - github.com/EMERALD0874
16 |
17 |
18 |
19 |
20 |
Support
21 |
22 | See the DeckThemes Discord server for support.
23 |
24 | discord.gg/HsU72Kfnpf
25 |
26 |
27 |
28 |
29 | Create and Submit Your Own Theme
30 |
31 |
32 | Instructions for theme creation/submission are available DeckThemes' docs website.
33 |
34 | docs.deckthemes.com
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Modals/DeleteConfirmationModal.tsx:
--------------------------------------------------------------------------------
1 | import { CssLoaderContextProvider } from "../../state";
2 | import * as python from "../../python";
3 | import { ConfirmModal, ModalRoot } from "decky-frontend-lib";
4 |
5 | export function DeleteConfirmationModalRoot({
6 | themesToBeDeleted,
7 | closeModal,
8 | leaveDeleteMode,
9 | }: {
10 | themesToBeDeleted: string[];
11 | closeModal?: any;
12 | leaveDeleteMode?: () => void;
13 | }) {
14 | async function deleteThemes() {
15 | for (let i = 0; i < themesToBeDeleted.length; i++) {
16 | await python.deleteTheme(themesToBeDeleted[i]);
17 | }
18 | await python.getInstalledThemes();
19 | leaveDeleteMode && leaveDeleteMode();
20 | closeModal();
21 | }
22 |
23 | return (
24 |
30 | {/* @ts-ignore */}
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | function DeleteConfirmationModal({ themesToBeDeleted }: { themesToBeDeleted: string[] }) {
39 | return (
40 |
41 | Are you sure you want to delete{" "}
42 | {themesToBeDeleted.length === 1 ? `this theme` : `these ${themesToBeDeleted.length} themes`}?
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/logic/bulkThemeUpdateCheck.ts:
--------------------------------------------------------------------------------
1 | import { Theme, UpdateStatus } from "../ThemeTypes";
2 | import { genericGET } from "../api";
3 | import { MinimalCSSThemeInfo } from "../apiTypes";
4 | import { globalState } from "../python";
5 | const apiUrl = "https://api.deckthemes.com";
6 |
7 | async function fetchThemeIDS(idsToQuery: string[]): Promise {
8 | const queryStr = "?ids=" + idsToQuery.join(".");
9 | return genericGET(`/themes/ids${queryStr}`)
10 | .then((data) => {
11 | if (data) return data;
12 | return [];
13 | })
14 | .catch((err) => {
15 | console.error("Error Fetching Theme Updates!", err);
16 | return [];
17 | });
18 | }
19 |
20 | export async function bulkThemeUpdateCheck(customThemeArr?: Theme[]) {
21 | const localThemeList = customThemeArr || globalState!.getPublicState().localThemeList;
22 | let idsToQuery: string[] = localThemeList.map((e) => e.id);
23 |
24 | if (idsToQuery.length === 0) return [];
25 |
26 | const themeArr = await fetchThemeIDS(idsToQuery);
27 |
28 | if (themeArr.length === 0) return [];
29 |
30 | const updateStatusArr: UpdateStatus[] = localThemeList.map((localEntry) => {
31 | const remoteEntry = themeArr.find(
32 | (remote) => remote.id === localEntry.id || remote.name === localEntry.id
33 | );
34 | if (!remoteEntry) return [localEntry.id, "local", false];
35 | if (remoteEntry.version === localEntry.version)
36 | return [localEntry.id, "installed", remoteEntry];
37 | return [localEntry.id, "outdated", remoteEntry];
38 | });
39 |
40 | return updateStatusArr;
41 | }
42 |
--------------------------------------------------------------------------------
/src/deckyPatches/ClassHashMap.tsx:
--------------------------------------------------------------------------------
1 | import { classMap } from "decky-frontend-lib";
2 |
3 | export var classHashMap = new Map();
4 |
5 | export function initializeClassHashMap() {
6 | const withoutLocalizationClasses = classMap.filter((module) => Object.keys(module).length < 1000);
7 |
8 | const allClasses = withoutLocalizationClasses
9 | .map((module) => {
10 | let filteredModule = {};
11 | Object.entries(module).forEach(([propertyName, value]) => {
12 | // Filter out things that start with a number (eg: Breakpoints like 800px)
13 | // I have confirmed the new classes don't start with numbers
14 | if (isNaN(Number(value.charAt(0)))) {
15 | filteredModule[propertyName] = value;
16 | }
17 | });
18 | return filteredModule;
19 | })
20 | .filter((module) => {
21 | // Some modules will be empty after the filtering, remove those
22 | return Object.keys(module).length > 0;
23 | });
24 |
25 | const mappings = allClasses.reduce((acc, cur) => {
26 | Object.entries(cur).forEach(([property, value]) => {
27 | if (acc[property]) {
28 | acc[property].push(value);
29 | } else {
30 | acc[property] = [value];
31 | }
32 | });
33 | return acc;
34 | }, {});
35 |
36 | const hashMapNoDupes = Object.entries(mappings).reduce