Merge branch 'master' of https://github.com/Team-BTMC/osu-radio into feature/color-changes

This commit is contained in:
dudubtw
2024-10-24 19:25:15 -03:00
33 changed files with 688 additions and 945 deletions

View File

@@ -35,7 +35,7 @@ jobs:
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Run Eslint - name: Run Eslint
run: npm run lint:check run: npm run lint
format: format:
timeout-minutes: 10 timeout-minutes: 10

View File

@@ -1,5 +0,0 @@
/** @type {import("prettier").Options} */
export default {
printWidth: 100,
plugins: ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
};

9
.prettierrc.ts Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Options} */
export default {
printWidth: 100,
plugins: [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss",
"prettier-plugin-packagejson",
],
};

21
eslint.config.js Normal file
View File

@@ -0,0 +1,21 @@
import eslint from "@eslint/js";
import solid from "eslint-plugin-solid";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["**/out", "**/build", "**/dist"],
},
{
files: ["**/*"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
{
files: ["src/renderer/**/*.tsx"],
...solid.configs["flat/recommended"],
},
);

View File

@@ -1,37 +0,0 @@
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ["**/out", "**/build", "**/dist"],
},
...compat.extends("plugin:@typescript-eslint/recommended"),
{
plugins: {
"@typescript-eslint": typescriptEslint,
},
languageOptions: {
parser: tsParser,
},
},
{
files: ["**/*"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
];

398
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@types/graceful-fs": "^4.1.6", "@types/graceful-fs": "^4.1.9",
"@xhayper/discord-rpc": "^1.2.0", "@xhayper/discord-rpc": "^1.2.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -32,25 +32,27 @@
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.11.1", "@eslint/js": "^9.13.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^22.7.4", "@types/eslint__eslintrc": "^2.1.2",
"@typescript-eslint/eslint-plugin": "^8.8.0", "@types/eslint__js": "^8.42.3",
"@typescript-eslint/parser": "^8.8.0", "@types/node": "^22.7.7",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"electron": "^32.1.2", "electron": "^32.1.2",
"electron-builder": "^25.1.7", "electron-builder": "^25.1.7",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"eslint": "^9.11.1", "eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-solid": "^0.14.3", "eslint-plugin-solid": "^0.14.3",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-packagejson": "^2.5.3",
"prettier-plugin-tailwindcss": "^0.6.8", "prettier-plugin-tailwindcss": "^0.6.8",
"solid-js": "^1.7.6", "solid-js": "^1.7.6",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"typescript": "~5.5.0", "typescript": "~5.6.3",
"typescript-eslint": "^8.10.0",
"vite": "^5.4.8", "vite": "^5.4.8",
"vite-plugin-lucide-preprocess": "^1.1.1", "vite-plugin-lucide-preprocess": "^1.1.1",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
@@ -1361,9 +1363,9 @@
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.6.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -1395,9 +1397,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.11.1", "version": "9.13.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1588,6 +1590,30 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@humanfs/core": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz",
"integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.5",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz",
"integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.0",
"@humanwhocodes/retry": "^0.3.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1603,9 +1629,9 @@
} }
}, },
"node_modules/@humanwhocodes/retry": { "node_modules/@humanwhocodes/retry": {
"version": "0.3.0", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -2780,6 +2806,37 @@
"@types/ms": "*" "@types/ms": "*"
} }
}, },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint__eslintrc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/eslint__eslintrc/-/eslint__eslintrc-2.1.2.tgz",
"integrity": "sha512-qXvzPFY7Rz05xD8ZApXJ3S8xStQD2Ibzu3EFIF0UMNOAfLY5xUu3H61q0JrHo2OXD6rcFG75yUxNQbkKtFKBSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*"
}
},
"node_modules/@types/eslint__js": {
"version": "8.42.3",
"resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz",
"integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -2854,9 +2911,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.7.4", "version": "22.7.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz",
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
@@ -2902,17 +2959,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz",
"integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.8.0", "@typescript-eslint/scope-manager": "8.10.0",
"@typescript-eslint/type-utils": "8.8.0", "@typescript-eslint/type-utils": "8.10.0",
"@typescript-eslint/utils": "8.8.0", "@typescript-eslint/utils": "8.10.0",
"@typescript-eslint/visitor-keys": "8.8.0", "@typescript-eslint/visitor-keys": "8.10.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -2936,16 +2993,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz",
"integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.8.0", "@typescript-eslint/scope-manager": "8.10.0",
"@typescript-eslint/types": "8.8.0", "@typescript-eslint/types": "8.10.0",
"@typescript-eslint/typescript-estree": "8.8.0", "@typescript-eslint/typescript-estree": "8.10.0",
"@typescript-eslint/visitor-keys": "8.8.0", "@typescript-eslint/visitor-keys": "8.10.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2965,14 +3022,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz",
"integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.8.0", "@typescript-eslint/types": "8.10.0",
"@typescript-eslint/visitor-keys": "8.8.0" "@typescript-eslint/visitor-keys": "8.10.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2983,14 +3040,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz",
"integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.8.0", "@typescript-eslint/typescript-estree": "8.10.0",
"@typescript-eslint/utils": "8.8.0", "@typescript-eslint/utils": "8.10.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -3008,9 +3065,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz",
"integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3022,14 +3079,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz",
"integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.8.0", "@typescript-eslint/types": "8.10.0",
"@typescript-eslint/visitor-keys": "8.8.0", "@typescript-eslint/visitor-keys": "8.10.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -3077,16 +3134,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz",
"integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.8.0", "@typescript-eslint/scope-manager": "8.10.0",
"@typescript-eslint/types": "8.8.0", "@typescript-eslint/types": "8.10.0",
"@typescript-eslint/typescript-estree": "8.8.0" "@typescript-eslint/typescript-estree": "8.10.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3100,13 +3157,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.8.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz",
"integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.8.0", "@typescript-eslint/types": "8.10.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -4681,6 +4738,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/detect-indent": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz",
"integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -4690,6 +4757,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/detect-newline": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/detect-node": { "node_modules/detect-node": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
@@ -4725,6 +4805,19 @@
"p-limit": "^3.1.0 " "p-limit": "^3.1.0 "
} }
}, },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.37.101", "version": "0.37.101",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.101.tgz", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.101.tgz",
@@ -5195,22 +5288,22 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.11.1", "version": "9.13.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz",
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0", "@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0", "@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0", "@eslint/core": "^0.7.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.11.1", "@eslint/js": "9.13.0",
"@eslint/plugin-kit": "^0.2.0", "@eslint/plugin-kit": "^0.2.0",
"@humanfs/node": "^0.16.5",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0", "@humanwhocodes/retry": "^0.3.1",
"@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
@@ -5218,9 +5311,9 @@
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.0.2", "eslint-scope": "^8.1.0",
"eslint-visitor-keys": "^4.0.0", "eslint-visitor-keys": "^4.1.0",
"espree": "^10.1.0", "espree": "^10.2.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@@ -5230,13 +5323,11 @@
"ignore": "^5.2.0", "ignore": "^5.2.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.9.3", "optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
"text-table": "^0.2.0" "text-table": "^0.2.0"
}, },
"bin": { "bin": {
@@ -5902,6 +5993,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-stdin": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
"integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -5917,6 +6021,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/git-hooks-list": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz",
"integrity": "sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -6000,6 +6114,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/globby": {
"version": "13.2.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz",
"integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"dir-glob": "^3.0.1",
"fast-glob": "^3.3.0",
"ignore": "^5.2.4",
"merge2": "^1.4.1",
"slash": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -6527,14 +6661,17 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-path-inside": { "node_modules/is-plain-obj": {
"version": "3.0.3", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-potential-custom-element-name": { "node_modules/is-potential-custom-element-name": {
@@ -7871,6 +8008,16 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pe-library": { "node_modules/pe-library": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
@@ -8160,6 +8307,25 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/prettier-plugin-packagejson": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.3.tgz",
"integrity": "sha512-ATMEEXr+ywls1kgrZEWl4SBPEm0uDdyDAjyNzUC0/Z8WZTD3RqbJcQDR+Dau+wYkW9KHK6zqQIsFyfn+9aduWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"sort-package-json": "2.10.1",
"synckit": "0.9.2"
},
"peerDependencies": {
"prettier": ">= 1.16.0"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
}
}
},
"node_modules/prettier-plugin-tailwindcss": { "node_modules/prettier-plugin-tailwindcss": {
"version": "0.6.8", "version": "0.6.8",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz",
@@ -8876,6 +9042,19 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/slash": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
"integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/slice-ansi": { "node_modules/slice-ansi": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
@@ -8984,6 +9163,33 @@
"solid-js": "^1.3" "solid-js": "^1.3"
} }
}, },
"node_modules/sort-object-keys": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz",
"integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==",
"dev": true,
"license": "MIT"
},
"node_modules/sort-package-json": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.10.1.tgz",
"integrity": "sha512-d76wfhgUuGypKqY72Unm5LFnMpACbdxXsLPcL27pOsSrmVqH3PztFp1uq+Z22suk15h7vXmTesuh2aEjdCqb5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-indent": "^7.0.1",
"detect-newline": "^4.0.0",
"get-stdin": "^9.0.0",
"git-hooks-list": "^3.0.0",
"globby": "^13.1.2",
"is-plain-obj": "^4.1.0",
"semver": "^7.6.0",
"sort-object-keys": "^1.1.3"
},
"bin": {
"sort-package-json": "cli.js"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -9281,9 +9487,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/synckit": { "node_modules/synckit": {
"version": "0.9.1", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
"integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -9612,9 +9818,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.4", "version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -9625,6 +9831,30 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/typescript-eslint": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.10.0.tgz",
"integrity": "sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.10.0",
"@typescript-eslint/parser": "8.10.0",
"@typescript-eslint/utils": "8.10.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/undici": { "node_modules/undici": {
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",

View File

@@ -2,32 +2,32 @@
"name": "osu-radio", "name": "osu-radio",
"version": "1.0.0", "version": "1.0.0",
"description": "Application to play your osu! songs.", "description": "Application to play your osu! songs.",
"main": "./out/main/index.js",
"type": "module",
"author": "@CaptSiro",
"homepage": "https://github.com/Team-BTMC/osu-radio", "homepage": "https://github.com/Team-BTMC/osu-radio",
"author": "@CaptSiro",
"type": "module",
"main": "./out/main/index.js",
"scripts": { "scripts": {
"format": "prettier --write .", "build": "npm run typecheck && electron-vite build",
"format:check": "prettier --check .", "build:linux": "electron-vite build && electron-builder --linux --config",
"lint": "eslint . --fix", "build:mac": "electron-vite build && electron-builder --mac --config",
"lint:check": "eslint .", "build:win": "npm run build && electron-builder --win --config",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"dev:watch": "electron-vite dev --watch", "dev:watch": "electron-vite dev --watch",
"build": "npm run typecheck && electron-vite build", "format": "prettier --write .",
"format:check": "prettier --check .",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"build:win": "npm run build && electron-builder --win --config", "lint": "eslint .",
"build:mac": "electron-vite build && electron-builder --mac --config", "lint:fix": "eslint . --fix",
"build:linux": "electron-vite build && electron-builder --linux --config" "start": "electron-vite preview",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false"
}, },
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@types/graceful-fs": "^4.1.6", "@types/graceful-fs": "^4.1.9",
"@xhayper/discord-rpc": "^1.2.0", "@xhayper/discord-rpc": "^1.2.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -47,25 +47,27 @@
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.11.1", "@eslint/js": "^9.13.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^22.7.4", "@types/eslint__eslintrc": "^2.1.2",
"@typescript-eslint/eslint-plugin": "^8.8.0", "@types/eslint__js": "^8.42.3",
"@typescript-eslint/parser": "^8.8.0", "@types/node": "^22.7.7",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"electron": "^32.1.2", "electron": "^32.1.2",
"electron-builder": "^25.1.7", "electron-builder": "^25.1.7",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"eslint": "^9.11.1", "eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-solid": "^0.14.3", "eslint-plugin-solid": "^0.14.3",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-packagejson": "^2.5.3",
"prettier-plugin-tailwindcss": "^0.6.8", "prettier-plugin-tailwindcss": "^0.6.8",
"solid-js": "^1.7.6", "solid-js": "^1.7.6",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"typescript": "~5.5.0", "typescript": "~5.6.3",
"typescript-eslint": "^8.10.0",
"vite": "^5.4.8", "vite": "^5.4.8",
"vite-plugin-lucide-preprocess": "^1.1.1", "vite-plugin-lucide-preprocess": "^1.1.1",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"

9
src/@types.d.ts vendored
View File

@@ -223,15 +223,6 @@ export type SongViewProps = {
playlist?: string; playlist?: string;
}; };
export type NoticeType = {
id?: string;
class: "notice" | "warning" | "error";
title: string;
content: string;
timeoutMS?: number;
active?: boolean;
};
export type InfiniteScrollerRequest = { export type InfiniteScrollerRequest = {
index: number; index: number;
init: number; init: number;

View File

@@ -51,14 +51,14 @@ export class TemplateTokenizer {
switch (this.char) { switch (this.char) {
case "\0": case "\0":
return createToken(Tokens.EOF, this.char, this.position); return createToken(Tokens.EOF, this.char, this.position);
case "\\": case "\\": {
const next = this.peek(); const next = this.peek();
if (next === "{" || next === "}") { if (next === "{" || next === "}") {
this.readChar(); this.readChar();
return createToken(Tokens.Text, this.char, this.position); return createToken(Tokens.Text, this.char, this.position);
} }
return createToken(Tokens.Text, this.char, this.position); return createToken(Tokens.Text, this.char, this.position);
}
case "{": case "{":
return createToken(Tokens.LeftSquirly, this.char, this.position); return createToken(Tokens.LeftSquirly, this.char, this.position);
case "}": case "}":

View File

@@ -52,7 +52,7 @@ async function configureOsuDir(mainWindow: BrowserWindow) {
let tables: Awaited<DirParseResult>; let tables: Awaited<DirParseResult>;
const settings = Storage.getTable("settings"); const settings = Storage.getTable("settings");
do { while (true) {
await Router.dispatch(mainWindow, "changeScene", "dir-select"); await Router.dispatch(mainWindow, "changeScene", "dir-select");
const dir = await dirSubmit(); const dir = await dirSubmit();
@@ -91,7 +91,7 @@ async function configureOsuDir(mainWindow: BrowserWindow) {
// All went smoothly. Save osu directory and continue with import procedure // All went smoothly. Save osu directory and continue with import procedure
settings.write("osuSongsDir", dir); settings.write("osuSongsDir", dir);
break; break;
} while (true); }
// Show finished state // Show finished state
await Router.dispatch(mainWindow, "loadingScene::update", { await Router.dispatch(mainWindow, "loadingScene::update", {

View File

@@ -1,15 +0,0 @@
.notice-container-wrapper {
position: fixed;
top: 0;
right: 0;
width: clamp(250px, 400px, 33vw);
overflow: auto;
max-height: 100vh;
}
.notice-container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 16px 16px 32px;
}

View File

@@ -1,58 +0,0 @@
.notice-wrapper {
overflow: hidden;
border-radius: var(--border-radius);
}
.notice {
background-color: rgba(var(--color-fg), var(--level-0));
width: 100%;
height: 100%;
}
.notice.warning {
background-color: oklch(70% 0.15 55 / var(--level-0));
}
.notice.error {
background-color: oklch(70% 0.15 20 / var(--level-0));
}
.notice .content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.notice .content .head {
display: flex;
gap: 8px;
justify-content: space-between;
}
.notice .timeout {
height: 8px;
width: 100%;
background-color: rgba(var(--color-fg), var(--level-3));
}
.notice.warning .timeout {
background-color: oklch(70% 0.15 55 / var(--level-3));
}
.notice.error .timeout {
background-color: oklch(70% 0.15 20 / var(--level-3));
}
.notice .timeout.pause {
animation-play-state: paused;
}
@keyframes drain-time {
from {
width: 100%;
}
to {
width: 0;
}
}

View File

@@ -1,86 +0,0 @@
import "../assets/css/bar.css";
import { clamp } from "../lib/tungsten/math";
import { Component, createEffect, createSignal, onMount } from "solid-js";
type BarAlignment = "vertical" | "v" | "horizontal" | "h";
function isVertical(alignment?: BarAlignment): boolean {
return alignment === "vertical" || alignment === "v";
}
type BarProps = {
alignment?: BarAlignment;
fill: number;
setFill?: (fill: number) => any;
disabled?: boolean;
};
const Bar: Component<BarProps> = (props) => {
const [fill, setFill] = createSignal(props.fill);
let bar: HTMLDivElement | undefined, handle: HTMLDivElement | undefined;
onMount(() => {
createEffect(() => {
const f = fill();
bar?.style.setProperty("--fill-per", `${clamp(0, 1, f) * 100}%`);
if (props.setFill !== undefined) {
props.setFill(clamp(0, 1, f));
}
});
});
const calculateFill = (evt: PointerEvent) => {
if (props.disabled === true || !bar) {
return;
}
const rect: DOMRect = bar.getBoundingClientRect();
if (isVertical(props.alignment)) {
setFill(clamp(0, 1, -(evt.clientY - rect.top) / rect.height + 1));
return;
}
setFill(clamp(0, 1, (evt.clientX - rect.left) / rect.width));
};
const onDown = (evt: PointerEvent) => {
if (props.disabled === true) {
return;
}
handle?.setPointerCapture(evt.pointerId);
handle?.addEventListener("pointermove", calculateFill);
handle?.addEventListener(
"pointerup",
() => handle.removeEventListener("pointermove", calculateFill),
{ once: true },
);
};
return (
<div
ref={bar}
classList={{
bar: true,
vertical: props.alignment !== undefined,
editable: props.setFill !== undefined,
}}
style={{
"--fill-per": clamp(0, 1, props.fill) * 100 + "%",
}}
onPointerDown={calculateFill}
data-disabled={props.disabled}
>
<div class="filling-container">
<div class="filling"></div>
</div>
<div ref={handle} class="handle" onPointerDown={onDown}></div>
</div>
);
};
export default Bar;

View File

@@ -1,59 +0,0 @@
import "../assets/css/gradient.css";
import Impulse from "../lib/Impulse";
import { Component, JSXElement, onCleanup, onMount } from "solid-js";
export type GradientColors = {
top: string;
bottom: string;
};
type GradientProps = {
classTop?: string;
classBottom?: string;
update?: Impulse;
children: JSXElement;
};
const Gradient: Component<GradientProps> = (props) => {
let bottomLayer: HTMLDivElement | undefined;
const calculateBackground = () => {
if (!bottomLayer) {
return;
}
const rect: DOMRect = bottomLayer.getBoundingClientRect();
bottomLayer.style.setProperty("--left", `${Math.round(-rect.left)}px`);
bottomLayer.style.setProperty("--top", `${Math.round(-rect.top)}px`);
bottomLayer.style.setProperty("--right", `${Math.round(rect.left + rect.width)}px`);
bottomLayer.style.setProperty("--bottom", `${Math.round(rect.top + rect.height)}px`);
bottomLayer.style.setProperty("--width", `${Math.round(window.innerWidth * 1.25)}px`);
bottomLayer.style.setProperty("--height", `${Math.round(window.innerHeight * 1.25)}px`);
};
if (props.update !== undefined) {
props.update.listen(calculateBackground);
}
onMount(() => {
calculateBackground();
window.addEventListener("resize", calculateBackground);
});
onCleanup(() => {
window.removeEventListener("resize", calculateBackground);
});
return (
<div ref={bottomLayer} class={`bottom-layer ${props.classBottom ?? ""}`}>
<div class={`gradient-layer`}>
<div class={`top-layer ${props.classTop ?? ""}`}>{props.children}</div>
</div>
</div>
);
};
export default Gradient;

View File

@@ -1,49 +0,0 @@
import "../assets/css/select.css";
import { Component, For, onMount, Setter } from "solid-js";
export type SelectOption = {
value: string;
text: string;
selected?: boolean;
};
type SelectProps = {
setValue: Setter<string>;
options: SelectOption[];
selected?: string;
disabled?: boolean;
};
const Select: Component<SelectProps> = (props) => {
let select: HTMLSelectElement | undefined;
onMount(() => {
if (!select) {
return;
}
props.setValue(select.value);
});
return (
<select
class={"button-like select"}
ref={select}
onchange={() => select && props.setValue(select.value)}
disabled={props.disabled}
>
<For each={props.options}>
{(option) => (
<option
value={option.value}
selected={option.selected === true || option.value === props.selected}
>
{option.text}
</option>
)}
</For>
</select>
);
};
export default Select;

View File

@@ -1,85 +0,0 @@
import { Component, JSX, onMount, Setter, Signal } from "solid-js";
type TextFieldProps = {
value: Signal<string>;
setInput?: Setter<HTMLElement | undefined>;
children?: JSX.Element;
};
const TextField: Component<TextFieldProps> = (props) => {
const [value, setValue] = props.value;
let input: HTMLInputElement | undefined;
onMount(() => {
if (!input) {
return;
}
if (props.setInput !== undefined) {
props.setInput(input);
}
input.textContent = value();
});
const onInput = () => {
if (!input) {
return;
}
setValue(
String(input.textContent).replaceAll(
String.fromCharCode(160), // non-breaking space
String.fromCharCode(32), // breaking space
) ?? "",
);
};
const onPaste = (evt: ClipboardEvent) => {
const selection = window.getSelection();
if (selection === null || !evt.clipboardData) return;
evt.stopPropagation();
evt.preventDefault();
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(document.createTextNode(evt.clipboardData.getData("Text")));
selection.collapseToEnd();
onInput();
};
const clear = () => {
if (!input) {
return;
}
input.textContent = "";
onInput();
input.focus();
};
return (
<div class="button-like flex w-full items-center gap-2 overflow-hidden p-2 focus-within:outline-2 focus-within:transition-none hover:cursor-text">
{props.children}
<div
class="flex-1 overflow-hidden whitespace-nowrap focus:outline-none"
ref={input}
onInput={onInput}
onKeyDown={(evt) => evt.stopPropagation()}
onPaste={onPaste}
contenteditable={true}
spellcheck={false}
></div>
<button
class="mr-1 grid place-items-center bg-transparent p-0 hover:cursor-pointer hover:bg-transparent focus:outline-none"
onClick={clear}
title="Clear text input"
>
{/* TODO: Add clear icon */}
</button>
</div>
);
};
export default TextField;

View File

@@ -1,109 +1,188 @@
import { Optional } from "../../../../@types"; import Button from "../button/Button";
import "../../assets/css/notice/notice.css"; import { cva } from "class-variance-authority";
import Impulse from "../../lib/Impulse";
import { none, orDefault, some } from "../../lib/rust-like-utils-client/Optional";
import Gradient from "../Gradient";
import { hideNotice } from "./NoticeContainer";
import { XIcon } from "lucide-solid"; import { XIcon } from "lucide-solid";
import { Accessor, Component, createSignal } from "solid-js"; import { Component, createEffect, createMemo, createSignal, JSX, onMount, Show } from "solid-js";
import { DOMElement } from "solid-js/jsx-runtime";
import { twMerge } from "tailwind-merge";
const bgStyle = cva([], {
variants: {
variant: {
success: "bg-green after:bg-green",
neutral: "bg-overlay after:bg-overlay",
error: "bg-red after:bg-red",
},
},
defaultVariants: {
variant: "neutral",
},
});
const textStyle = cva([], {
variants: {
variant: {
success: "text-green",
neutral: "text-subtext",
error: "text-red",
},
},
defaultVariants: {
variant: "neutral",
},
});
export type NoticeType = { export type NoticeType = {
id?: string; id?: string;
class: "notice" | "warning" | "error"; variant?: "neutral" | "success" | "error";
title: string; title?: string;
content: string; description?: string;
timeoutMS?: number; icon?: JSX.Element;
active?: boolean;
}; };
type NoticeProps = { type NoticeProps = {
notice: NoticeType; notice: NoticeType;
updateGradient: Impulse; onRemove: (id: string) => void;
onMount: (notice: HTMLElement) => any; isPaused: boolean;
}; };
const Notice: Component<NoticeProps> = (props) => { const NOTICE_DURATION = 3_000; // 3 seconds
const removeNotice = () => {
const action = hideNotice(props.notice.id);
if (action.isError) { const Notice: Component<NoticeProps> = (props) => {
const [isRemoving, setIsRemoving] = createSignal(false);
let noticeRef: HTMLDivElement | undefined;
let timeout: NodeJS.Timeout;
let startTime: number;
let pausedTime: number = 0;
const removeNotice = () => {
setIsRemoving(true);
};
const startRemoveTimeout = () => {
startTime = Date.now();
timeout = setTimeout(removeNotice, NOTICE_DURATION);
};
const pauseRemoveTimeout = () => {
pausedTime = Date.now() - startTime;
};
const resumeRemoveTimeout = () => {
startTime = Date.now() - pausedTime;
timeout = setTimeout(removeNotice, NOTICE_DURATION - pausedTime);
};
onMount(() => {
startRemoveTimeout();
});
createEffect(() => {
clearTimeout(timeout);
if (props.isPaused) {
pauseRemoveTimeout();
} else {
resumeRemoveTimeout();
}
});
const handleAnimationEnd = (
event: AnimationEvent & {
currentTarget: HTMLDivElement;
target: DOMElement;
},
) => {
// Validates if the animation finidhed on the current element
if (event.target !== event.currentTarget) {
return; return;
} }
try { if (!isRemoving()) {
pauseDrain(); return;
} catch {} }
props.onRemove(props.notice.id!);
}; };
const [drain, setDrainTime, pauseDrain] = createDrainAnimation( const removingClasses = createMemo(() => {
props.notice.timeoutMS ?? 10_000, if (isRemoving()) {
removeNotice, return "animate-notice-slide-out";
); }
return "my-2 min-h-20";
const onRef = (notice: HTMLElement) => { });
props.updateGradient.pulse();
props.onMount(notice);
};
return ( return (
<div <div
class={"notice-wrapper"} ref={noticeRef}
onPointerEnter={pauseDrain} onAnimationEnd={handleAnimationEnd}
onPointerLeave={() => setDrainTime(props.notice.timeoutMS ?? 10_000)} class={twMerge(
// Background
bgStyle({ variant: props.notice.variant }),
// General
"group relative w-96 transform overflow-hidden rounded-xl bg-thick-material p-4 shadow-2xl ring-2 ring-inset ring-stroke backdrop-blur-md duration-300 ease-in-out",
// After
`after:absolute after:inset-0 after:-z-20 after:size-20 after:rounded-full after:blur-2xl after:content-['']`,
// Before
`before:absolute before:inset-0.5 before:-z-10 before:rounded-[10px] before:bg-thick-material before:content-['']`,
// Enter animation
"animate-notice-slide-in",
// Removing
removingClasses(),
)}
data-id={props.notice.id} data-id={props.notice.id}
ref={onRef}
> >
<Gradient update={props.updateGradient}> <Button
<div class={"notice " + (props.notice.class !== "notice" ? props.notice.class : "")}> variant="ghost"
<div class="content"> size="icon"
<div class="head"> onClick={removeNotice}
<h3>{props.notice.title}</h3> class="absolute right-4 top-4 size-5 p-1 text-subtext opacity-0 transition-opacity duration-150 group-hover:opacity-100"
<button onClick={removeNotice}> >
<XIcon size={20} /> <XIcon size={16} />
</button> </Button>
</div>
<p>{props.notice.content}</p>
</div>
<div class="mr-6 flex items-start">
<Show when={props.notice.icon}>
<div <div
class="timeout" class={twMerge(
classList={{ "mr-3 mt-0.5 flex-shrink-0",
pause: drain().isNone, textStyle({ variant: props.notice.variant }),
}} )}
style={{ >
animation: orDefault(drain(), undefined), {props.notice.icon}
}} </div>
></div> </Show>
<div class="overflow-hidden">
<Show when={props.notice.title}>
<h3 class="mb-1 overflow-hidden text-ellipsis whitespace-nowrap text-base font-semibold">
{props.notice.title}
</h3>
</Show>
<Show when={props.notice.description}>
<p class="line-clamp-2 text-ellipsis text-sm text-subtext">
{props.notice.description}
</p>
</Show>
</div> </div>
</Gradient> </div>
<div
class="absolute inset-0 m-px overflow-hidden pointer-events-none"
style={{
"border-radius": "13px",
}}
>
<div
class={twMerge(
"absolute bottom-px left-0 h-1 rounded-full",
bgStyle({ variant: props.notice.variant }),
)}
style={{
animation: `progress ${NOTICE_DURATION}ms linear`,
"animation-play-state": props.isPaused ? "paused" : "running",
}}
/>
</div>
</div> </div>
); );
}; };
function createDrainAnimation(
timeMS: number,
onDrained: () => any,
): [Accessor<Optional<string>>, (timeMS: number) => any, () => any] {
const [get, set] = createSignal<Optional<string>>(some(drainTemplate(timeMS)));
let timeout = window.setTimeout(onDrained, timeMS);
return [
get,
(ms: number) => {
set(some(drainTemplate(ms)));
timeout = window.setTimeout(onDrained, ms);
},
() => {
set(none());
clearTimeout(timeout);
},
];
}
function drainTemplate(timeMS: number): string {
return `drain-time ${Math.round(timeMS)}ms linear forwards`;
}
export default Notice; export default Notice;

View File

@@ -0,0 +1,35 @@
export const noticeAnimations = {
keyframes: {
"notice-slide-in": {
from: { transform: "translateY(-100%)", filter: "blur(4px)", opacity: 0 },
to: { transform: "translateX(0)", opacity: 1 },
},
"notice-progress": {
from: { width: "100%" },
to: { width: "0%" },
},
"notice-slide-out": {
from: {
minHeight: "5rem",
marginBlock: "0.5rem",
},
to: {
"padding-block": "0",
"margin-block": "0",
height: "0",
"min-height": "0",
"max-height": "0",
transform: "scale(0.75)",
"transform-origin": "top right",
opacity: 0,
filter: "blur(4px)",
},
},
},
animation: {
"notice-slide-in": "notice-slide-in 300ms cubic-bezier(0.4, 0, 0.2, 1) forwards",
// TODO: Make this 3000ms configurable
"notice-progress": "notice-progress 3000ms linear",
"notice-slide-out": "notice-slide-out 300ms cubic-bezier(0.4, 0, 0.2, 1) forwards",
},
};

View File

@@ -1,112 +1,61 @@
import { Result } from "../../../../@types"; import { Result } from "../../../../@types";
import "../../assets/css/notice/notice-container.css";
import Impulse from "../../lib/Impulse";
import { fail, ok } from "../../lib/rust-like-utils-client/Result"; import { fail, ok } from "../../lib/rust-like-utils-client/Result";
import { TokenNamespace } from "../../lib/tungsten/token"; import { TokenNamespace } from "../../lib/tungsten/token";
import Notice, { NoticeType } from "./Notice"; import Notice, { NoticeType } from "./Notice";
import { For, onMount } from "solid-js"; import { For, createSignal } from "solid-js";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
type NoticeExtended = { const [notices, setNotices] = createStore<NoticeType[]>([]);
notice: NoticeType;
updateGradient: Impulse;
visible: boolean;
};
const [notices, setNotices] = createStore<NoticeExtended[]>([]);
const namespace = new TokenNamespace(); const namespace = new TokenNamespace();
const [isPaused, setIsPaused] = createSignal(false);
export function addNotice(notice: NoticeType): void { export function addNotice(notice: NoticeType): void {
if (notice.id === undefined) { if (notice.id === undefined) {
notice.id = namespace.create(); notice.id = namespace.create();
} }
setNotices([ setNotices([
...notices, ...notices,
{ {
notice, ...notice,
updateGradient: new Impulse<void>(), variant: notice.variant || "neutral",
visible: false,
}, },
]); ]);
} }
export function hideNotice(id: string | undefined): Result<void, string> { function hideNotice(id: string | undefined): Result<void, string> {
if (id === undefined) { if (id === undefined) {
return fail("Passed undefined ID."); return fail("Passed undefined ID.");
} }
setNotices((notices) => notices.filter((n) => n.id !== id));
setNotices(
(ex) => ex.notice.id === id,
"notice",
"active",
() => false,
);
return ok(undefined); return ok(undefined);
} }
export { notices }; export { notices, isPaused, setIsPaused };
const observer = new IntersectionObserver((entries) => { window.api.listen("notify", (n: NoticeType) => {
for (const entry of entries) { addNotice(n);
const el = entry.target;
if (!(el instanceof HTMLElement)) {
return;
}
const id = el.dataset.id;
if (id === undefined) {
return;
}
setNotices((ex) => ex.notice.id === id, "visible", entry.isIntersecting);
}
});
window.api.listen("notify", (n) => {
if (n.id === undefined) {
n.id = namespace.create();
}
setNotices([
...notices,
{
notice: n,
updateGradient: new Impulse<void>(),
visible: false,
},
]);
}); });
const NoticeContainer = () => { const NoticeContainer = () => {
let wrapper: HTMLDivElement | undefined; const handleRemove = (id: string) => {
hideNotice(id);
onMount(() => { };
wrapper?.addEventListener("scroll", () => {
for (let i = 0; i < notices.length; i++) {
if (notices[i].visible) {
notices[i].updateGradient.pulse();
}
}
});
});
return ( return (
<div class={"notice-container-wrapper"} ref={wrapper}> <div
<div class={"notice-container"}> class="fixed right-4 top-16 z-50 flex w-96 flex-col gap-0"
<For each={notices.filter((n) => n.notice.active !== false)}> onMouseEnter={() => setIsPaused(true)}
{(n) => ( onMouseLeave={() => setIsPaused(false)}
<Notice >
notice={n.notice} <style>{`
updateGradient={n.updateGradient} @keyframes progress {
onMount={(e) => observer.observe(e)} from { width: 100%; }
/> to { width: 0%; }
)} }
</For> `}</style>
</div> <For each={notices}>
{(n) => <Notice notice={n} onRemove={handleRemove} isPaused={isPaused()} />}
</For>
</div> </div>
); );
}; };

View File

@@ -1,3 +1,3 @@
export default function NoScene() { export default function NoScene() {
return <div class="scene"></div>; return <div class="scene" />;
} }

View File

@@ -1,35 +0,0 @@
import "../../assets/css/search/tag-item.css";
import { XIcon } from "lucide-solid";
import { Component } from "solid-js";
export type TagItemProps = {
name: string;
isSpecial: boolean;
onRemove: (name: string) => any;
onChange: (name: string) => any;
};
const TagItem: Component<TagItemProps> = (props) => {
let container;
const changeState = (evt: Event) => {
evt.preventDefault();
props.onChange(props.name);
};
return (
<div
ref={container}
class={"tag"}
classList={{ special: props.isSpecial === true }}
onContextMenu={changeState}
>
<span>{props.name}</span>
<button onClick={() => props.onRemove(props.name)}>
<XIcon size={20} />
</button>
</div>
);
};
export default TagItem;

View File

@@ -1,160 +0,0 @@
import "../../assets/css/search/tag-select.css";
import Gradient from "../Gradient";
import TextField from "../form/TextField";
import TagItem from "./TagItem";
import { TagIcon } from "lucide-solid";
import { Component, createEffect, createSignal, For, Signal } from "solid-js";
export type Tag = {
name: string;
isSpecial?: boolean;
};
type TagSelectProps = {
/** Must have ```json
* { equals: false }
* ``` */
tags: Signal<Tag[]>;
disabled?: boolean;
};
const TagSelect: Component<TagSelectProps> = (props) => {
const [tags, setTags] = props.tags;
const tagSignal = createSignal("");
const [tagField, setTagField] = createSignal<HTMLElement | undefined>();
let dialog: HTMLDialogElement | undefined;
const openDialog = () => {
dialog?.showModal();
window.dispatchEvent(new Event("resize"));
};
const closeDialog = () => {
dialog?.close();
const field = tagField();
if (field === undefined) {
return;
}
field.textContent = "";
};
const onKeyDown = (evt: KeyboardEvent) => {
if (evt.key !== "Enter") {
return;
}
const input = tagField();
if (input === undefined) {
return;
}
evt.preventDefault();
evt.stopPropagation();
if (input.textContent === null) {
return;
}
const newTags = input.textContent.trim().split(" ");
const oldTags = tags();
for (let i = 0; i < newTags.length; i++) {
if (newTags[i] === "") {
continue;
}
const index = oldTags.findIndex((t) => t.name === newTags[i]);
if (index !== -1) {
continue;
}
oldTags.push({ name: newTags[i] });
}
setTags(oldTags);
input.textContent = "";
input.focus();
};
createEffect(() => {
const input = tagField();
if (input === undefined) {
return;
}
input.addEventListener("keydown", onKeyDown);
});
const onTagRemove = (name: string) => {
const t = tags();
const index = t.findIndex((x) => x.name === name);
if (index === -1) {
return;
}
t.splice(index, 1);
setTags(t);
};
const onTagChange = (name: string) => {
const t = tags();
const index = t.findIndex((x) => x.name === name);
if (index === -1) {
return;
}
t[index] = {
name,
isSpecial: !t[index].isSpecial,
};
setTags(t);
};
return (
<div class={"tags"}>
<button
onClick={openDialog}
disabled={props.disabled}
title={"Add/Remove tags for searching"}
>
<TagIcon size={20} />
</button>
<dialog ref={dialog} class={"tag-select"}>
<Gradient classTop={"tag-select-container"}>
<TextField value={tagSignal} setInput={setTagField} />
<span class={"hint"}>
Type the name of a tag into input above and press enter to add it. After it is added you
can right-click it and change it to exclude songs with given tag.
</span>
<div class={"tags-container"}>
<For each={tags()}>
{(tag: Tag) => (
<TagItem
name={tag.name}
isSpecial={tag.isSpecial === true}
onRemove={onTagRemove}
onChange={onTagChange}
/>
)}
</For>
</div>
<button onClick={closeDialog}>Close</button>
</Gradient>
</dialog>
</div>
);
};
export default TagSelect;

View File

@@ -1,4 +1,4 @@
import { Component, Show } from "solid-js"; import { Component, For, Show } from "solid-js";
type SettingDropdownProps = { type SettingDropdownProps = {
label: string; label: string;
@@ -27,9 +27,9 @@ const SettingDropdown: Component<SettingDropdownProps> = (props) => {
disabled={props.disabled} disabled={props.disabled}
onChange={changeOption} onChange={changeOption}
> >
{Array.from(props.options.keys()).map((option) => ( <For each={Array.from(props.options.keys())}>
<option value={option}>{option}</option> {(option) => <option value={option}>{option}</option>}
))} </For>
</select> </select>
</Show> </Show>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { cn } from "../../lib/css.utils";
import Dropdown from "../dropdown/Dropdown"; import Dropdown from "../dropdown/Dropdown";
import { changeAudioDevice } from "@renderer/components/song/song.utils"; import { changeAudioDevice } from "@renderer/components/song/song.utils";
import { GlobeIcon, LucideIcon, Volume2Icon } from "lucide-solid"; import { GlobeIcon, LucideIcon, Volume2Icon } from "lucide-solid";
import { Component, createEffect, createSignal, JSX, onMount } from "solid-js"; import { Component, createEffect, createSignal, For, JSX, onMount } from "solid-js";
const Settings: Component = () => { const Settings: Component = () => {
return ( return (
@@ -22,14 +22,14 @@ type SettingsSectionProps = JSX.IntrinsicElements["div"] & {
Icon: LucideIcon; Icon: LucideIcon;
}; };
const SettingsSection: Component<SettingsSectionProps> = ({ title, Icon, children, ...rest }) => { const SettingsSection: Component<SettingsSectionProps> = (props) => {
return ( return (
<div class={cn("flex flex-col gap-6", rest.class)}> <div class={cn("flex flex-col gap-6", props.class)}>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Icon class="text-text opacity-70" size={16} /> <props.Icon class="text-text opacity-70" size={16} />
<h3 class="text-base text-text">{title}</h3> <h3 class="text-base text-text">{props.title}</h3>
</div> </div>
{children} {props.children}
</div> </div>
); );
}; };
@@ -39,13 +39,13 @@ type SettingProps = JSX.IntrinsicElements["div"] & {
name: string; name: string;
}; };
const Setting: Component<SettingProps> = ({ label, name, children, ...rest }) => { const Setting: Component<SettingProps> = (props) => {
return ( return (
<div class={cn("flex flex-col gap-2.5", rest.class)}> <div class={cn("flex flex-col gap-2.5", props.class)}>
<label class="text-sm font-semibold text-text" for={name}> <label class="text-sm font-semibold text-text" for={props.name}>
{label} {props.label}
</label> </label>
{children} {props.children}
</div> </div>
); );
}; };
@@ -85,9 +85,9 @@ const AudioDeviceSetting: Component = () => {
</Dropdown.SelectTrigger> </Dropdown.SelectTrigger>
<Dropdown.List value={selectedAudioDevice} onValueChange={handleValueChange}> <Dropdown.List value={selectedAudioDevice} onValueChange={handleValueChange}>
{Array.from(audioDevices().keys()).map((audioDevice) => ( <For each={Array.from(audioDevices().keys())}>
<Dropdown.Item value={audioDevice}>{audioDevice}</Dropdown.Item> {(audioDevice) => <Dropdown.Item value={audioDevice}>{audioDevice}</Dropdown.Item>}
))} </For>
</Dropdown.List> </Dropdown.List>
</Dropdown> </Dropdown>
</Setting> </Setting>

View File

@@ -1,10 +1,10 @@
export default function SongHint(): HTMLElement { export default function SongHint(): HTMLElement {
return ( return (
<div class="flex items-center rounded-md bg-black bg-opacity-80 p-2 shadow-lg"> <div class="flex items-center rounded-md bg-black bg-opacity-80 p-2 shadow-lg">
<div class="mr-3 h-12 w-12 rounded-md bg-gray-700"></div> <div class="mr-3 h-12 w-12 rounded-md bg-gray-700" />
<div class="flex flex-col"> <div class="flex flex-col">
<div class="mb-2 h-4 w-32 rounded bg-gray-700"></div> <div class="mb-2 h-4 w-32 rounded bg-gray-700" />
<div class="h-3 w-24 rounded bg-gray-600"></div> <div class="h-3 w-24 rounded bg-gray-600" />
</div> </div>
</div> </div>
) as HTMLElement; ) as HTMLElement;

View File

@@ -18,40 +18,31 @@ type SongItemProps = {
children?: any; children?: any;
}; };
const SongItem: Component<SongItemProps> = ({ const SongItem: Component<SongItemProps> = (props) => {
group,
onSelect,
song,
draggable: isDraggable,
onDrop,
selectable,
}) => {
let item: HTMLDivElement | undefined; let item: HTMLDivElement | undefined;
const [, setCoords] = createSignal<[number, number]>([0, 0], { equals: false }); const [, setCoords] = createSignal<[number, number]>([0, 0], { equals: false });
const { extractColorFromImage } = useColorExtractor(); const { extractColorFromImage } = useColorExtractor();
const { primaryColor, secondaryColor, processImage } = extractColorFromImage(song); const { primaryColor, secondaryColor, processImage } = extractColorFromImage(props.song);
onMount(() => { onMount(() => {
if (!item) return; if (!item) return;
// Initialize draggable functionality // Initialize draggable functionality
draggable(item, { draggable(item, {
onClick: ignoreClickInContextMenu(() => { onClick: ignoreClickInContextMenu(() => props.onSelect(props.song.path)),
onSelect(song.path); onDrop: props.onDrop ?? (() => {}),
}),
onDrop: onDrop ?? (() => {}),
createHint: SongHint, createHint: SongHint,
useOnlyAsOnClickBinder: !isDraggable || selectedSong().path === song.path, useOnlyAsOnClickBinder: !props.draggable || selectedSong().path === props.song.path,
}); });
if (selectable === true) { if (props.selectable === true) {
item.dataset.path = song.path; item.dataset.path = props.song.path;
} }
}); });
const isSelected = createMemo(() => { const isSelected = createMemo(() => {
return selectedSong().audio === song.audio; return selectedSong().audio === props.song.audio;
}); });
const borderColor = createMemo(() => { const borderColor = createMemo(() => {
@@ -90,13 +81,13 @@ const SongItem: Component<SongItemProps> = ({
<div <div
class="group relative isolate select-none rounded-lg" class="group relative isolate select-none rounded-lg"
ref={item} ref={item}
data-url={song.bg} data-url={props.song.bg}
onContextMenu={(evt) => setCoords([evt.clientX, evt.clientY])} onContextMenu={(evt) => setCoords([evt.clientX, evt.clientY])}
> >
<SongImage <SongImage
class={`absolute inset-0 z-[-1] h-full w-full rounded-md bg-cover bg-center bg-no-repeat`} class={`absolute inset-0 z-[-1] h-full w-full rounded-md bg-cover bg-center bg-no-repeat`}
src={song.bg} src={props.song.bg}
group={group} group={props.group}
onImageLoaded={processImage} onImageLoaded={processImage}
/> />
<div <div
@@ -105,8 +96,8 @@ const SongItem: Component<SongItemProps> = ({
background: backgrund(), background: backgrund(),
}} }}
> >
<h3 class="text-shadow text-[22px] font-extrabold leading-7">{song.title}</h3> <h3 class="text-shadow text-[22px] font-extrabold leading-7">{props.song.title}</h3>
<p class="text-base text-subtext">{song.artist}</p> <p class="text-base text-subtext">{props.song.artist}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,5 @@
import { Optional, Order } from "../../../../../@types"; import { Optional, Order, Tag } from "../../../../../@types";
import { SearchQueryError } from "../../../../../main/lib/search-parser/@search-types"; import { SearchQueryError } from "../../../../../main/lib/search-parser/@search-types";
import { Tag } from "../../search/TagSelect";
import { setSongsSearch } from "../song-list/song-list.utils"; import { setSongsSearch } from "../song-list/song-list.utils";
import SongListSearchOrderBy from "./SongListSearchOrderBy"; import SongListSearchOrderBy from "./SongListSearchOrderBy";
import { SearchIcon } from "lucide-solid"; import { SearchIcon } from "lucide-solid";

View File

@@ -1,7 +1,7 @@
import Button from "@renderer/components/button/Button"; import Button from "@renderer/components/button/Button";
import Dropdown from "@renderer/components/dropdown/Dropdown"; import Dropdown from "@renderer/components/dropdown/Dropdown";
import { ArrowDownAzIcon, ArrowUpZaIcon } from "lucide-solid"; import { ArrowDownAzIcon, ArrowUpZaIcon } from "lucide-solid";
import { Component, createMemo, createSignal, Match, Setter, Switch } from "solid-js"; import { Component, createMemo, createSignal, For, Match, Setter, Switch } from "solid-js";
import { OrderDirection, OrderOptions, Order } from "src/@types"; import { OrderDirection, OrderOptions, Order } from "src/@types";
type OrderOption = { type OrderOption = {
@@ -84,14 +84,16 @@ const SongListSearchOrderBy: Component<OrderSelectProps> = (props) => {
}} }}
value={option} value={option}
> >
{orderOptions.map((option) => ( <For each={orderOptions}>
<Dropdown.Item {(option) => (
class="px-4 py-2 transition-colors duration-200 hover:bg-accent/20" <Dropdown.Item
value={option.value} class="px-4 py-2 transition-colors duration-200 hover:bg-accent/20"
> value={option.value}
{option.text} >
</Dropdown.Item> {option.text}
))} </Dropdown.Item>
)}
</For>
</Dropdown.List> </Dropdown.List>
</Dropdown> </Dropdown>
</div> </div>

View File

@@ -16,17 +16,18 @@ export type SongViewProps = {
playlist?: string; playlist?: string;
}; };
const SongList: Component<SongViewProps> = (props) => { const DEFAULT_TAGS_VALUE: Tag[] = [];
const tagsSignal = createSignal<Tag[]>([], { equals: false }); const DEFAULT_ORDER_VALUE: Order = { option: "title", direction: "asc" };
const [tags] = tagsSignal;
const [order, setOrder] = createSignal<Order>({ option: "title", direction: "asc" }); const SongList: Component<SongViewProps> = (props) => {
const [tags, setTags] = createSignal(DEFAULT_TAGS_VALUE, { equals: false });
const [order, setOrder] = createSignal(DEFAULT_ORDER_VALUE);
const [count, setCount] = createSignal(0); const [count, setCount] = createSignal(0);
const [payload, setPayload] = createSignal<SongsQueryPayload>({ const [payload, setPayload] = createSignal<SongsQueryPayload>({
view: props, view: props,
order: order(), order: DEFAULT_ORDER_VALUE,
tags: tags(), tags: DEFAULT_TAGS_VALUE,
}); });
const [searchError, setSearchError] = createSignal<Optional<SearchQueryError>>(none(), { const [searchError, setSearchError] = createSignal<Optional<SearchQueryError>>(none(), {
@@ -75,7 +76,12 @@ const SongList: Component<SongViewProps> = (props) => {
return ( return (
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<div class="sticky top-0 z-10"> <div class="sticky top-0 z-10">
<SongListSearch tags={tagsSignal} setOrder={setOrder} count={count} error={searchError} /> <SongListSearch
tags={[tags, setTags]}
setOrder={setOrder}
count={count}
error={searchError}
/>
</div> </div>
<div class="flex-grow overflow-y-auto p-5 py-0"> <div class="flex-grow overflow-y-auto p-5 py-0">

View File

@@ -1,6 +1,7 @@
import { addNotice } from "../components/notice/NoticeContainer"; import { addNotice } from "../components/notice/NoticeContainer";
import { Keyboard } from "../lib/Keyboard"; import { Keyboard } from "../lib/Keyboard";
import { mainActiveTab, TABS } from "@renderer/scenes/main-scene/main.utils"; import { mainActiveTab, TABS } from "@renderer/scenes/main-scene/main.utils";
import { ShuffleIcon } from "lucide-solid";
Keyboard.register({ Keyboard.register({
key: "F2", key: "F2",
@@ -12,9 +13,9 @@ Keyboard.register({
} }
addNotice({ addNotice({
class: "notice",
title: "Shuffled", title: "Shuffled",
content: "Current queue have been shuffled", icon: <ShuffleIcon size={20} />,
description: "The current queue has been shuffled.",
}); });
}, },
}); });

View File

@@ -1,3 +1,3 @@
export default function NoScene() { export default function NoScene() {
return <div class="scene"></div>; return <div class="scene" />;
} }

View File

@@ -3,6 +3,7 @@ import SongList from "../../components/song/song-list/SongList";
import { mainActiveTab, setMainActiveTab, Tab, TABS } from "./main.utils"; import { mainActiveTab, setMainActiveTab, Tab, TABS } from "./main.utils";
import "./styles.css"; import "./styles.css";
import Button from "@renderer/components/button/Button"; import Button from "@renderer/components/button/Button";
import NoticeContainer from "@renderer/components/notice/NoticeContainer";
import Settings from "@renderer/components/settings/Settings"; import Settings from "@renderer/components/settings/Settings";
import SongImage from "@renderer/components/song/SongImage"; import SongImage from "@renderer/components/song/SongImage";
import SongQueue from "@renderer/components/song/song-queue/SongQueue"; import SongQueue from "@renderer/components/song/song-queue/SongQueue";
@@ -21,6 +22,7 @@ import {
JSXElement, JSXElement,
Match, Match,
onCleanup, onCleanup,
onMount,
Setter, Setter,
Show, Show,
Switch, Switch,
@@ -29,6 +31,7 @@ import {
const MainScene: Component = () => { const MainScene: Component = () => {
return ( return (
<div class="main-scene flex h-screen flex-col overflow-hidden"> <div class="main-scene flex h-screen flex-col overflow-hidden">
<NoticeContainer />
<Nav /> <Nav />
<main class="relative flex h-[calc(100vh-52px)]"> <main class="relative flex h-[calc(100vh-52px)]">
<TabContent /> <TabContent />
@@ -53,7 +56,7 @@ const Nav: Component = () => {
const [os, setOs] = createSignal<NodeJS.Platform>(); const [os, setOs] = createSignal<NodeJS.Platform>();
const [maximized, setMaximized] = createSignal<boolean>(false); const [maximized, setMaximized] = createSignal<boolean>(false);
createEffect(async () => { onMount(async () => {
const fetchOS = async () => { const fetchOS = async () => {
return await window.api.request("os::platform"); return await window.api.request("os::platform");
}; };
@@ -64,10 +67,17 @@ const Nav: Component = () => {
setOs(await fetchOS()); setOs(await fetchOS());
setMaximized(await fetchMaximized()); setMaximized(await fetchMaximized());
});
window.api.listen("window::maximizeChange", (maximized: boolean) => { createEffect(() => {
const resizeListener = (maximized: boolean) => {
setMaximized(maximized); setMaximized(maximized);
}); };
window.api.listen("window::maximizeChange", resizeListener);
return () => {
window.api.removeListener("window::maximizeChange", resizeListener);
};
}); });
return ( return (
@@ -101,13 +111,13 @@ function WindowControls(props: { maximized: Accessor<boolean>; setMaximized: Set
return ( return (
<div class="nav-window-controls"> <div class="nav-window-controls">
<button <button
onclick={async () => window.api.request("window::minimize")} onClick={async () => window.api.request("window::minimize")}
class="nav-window-control" class="nav-window-control"
> >
<MinusIcon size={20} /> <MinusIcon size={20} />
</button> </button>
<button <button
onclick={async () => { onClick={async () => {
window.api.request("window::maximize"); window.api.request("window::maximize");
props.setMaximized(!props.maximized()); props.setMaximized(!props.maximized());
}} }}
@@ -116,7 +126,7 @@ function WindowControls(props: { maximized: Accessor<boolean>; setMaximized: Set
{props.maximized() ? <Minimize2Icon size={20} /> : <SquareIcon size={18} />} {props.maximized() ? <Minimize2Icon size={20} /> : <SquareIcon size={18} />}
</button> </button>
<button <button
onclick={async () => window.api.request("window::close")} onClick={async () => window.api.request("window::close")}
class="nav-window-control close" class="nav-window-control close"
> >
<XIcon size={20} /> <XIcon size={20} />
@@ -128,19 +138,19 @@ function WindowControls(props: { maximized: Accessor<boolean>; setMaximized: Set
type NavItemProps = Pick<Tab, "value" | "Icon"> & { type NavItemProps = Pick<Tab, "value" | "Icon"> & {
children: JSXElement; children: JSXElement;
}; };
const NavItem: Component<NavItemProps> = ({ children, value, Icon }) => { const NavItem: Component<NavItemProps> = (props) => {
return ( return (
<button <button
class={`nav-item flex items-center gap-4 rounded-sm px-4 py-1 hover:bg-surface ${mainActiveTab() === value ? "bg-surface" : ""}`} class={`nav-item flex items-center gap-4 rounded-sm px-4 py-1 hover:bg-surface ${mainActiveTab() === props.value ? "bg-surface" : ""}`}
onclick={() => setMainActiveTab(value)} onClick={() => setMainActiveTab(props.value)}
> >
<span class={`${mainActiveTab() === value ? "" : "opacity-70"}`}> <span class={`${mainActiveTab() === props.value ? "" : "opacity-70"}`}>
<Icon size={20} /> <props.Icon size={20} />
</span> </span>
<span <span
class={`text-base font-semibold ${mainActiveTab() === value ? "text-text" : "text-subtext"}`} class={`text-base font-semibold ${mainActiveTab() === props.value ? "text-text" : "text-subtext"}`}
> >
{children} {props.children}
</span> </span>
</button> </button>
); );

View File

@@ -1,9 +1,16 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
import { noticeAnimations } from "./src/renderer/src/components/notice/NoticeAnimations";
export default { export default {
content: ["./src/**/*.{js,ts,jsx,tsx}"], content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: { theme: {
extend: { extend: {
keyframes: {
...noticeAnimations.keyframes,
},
animation: {
...noticeAnimations.animation,
},
colors: { colors: {
transparent: "transparent", transparent: "transparent",
"thick-material": "rgba(var(--color-thick-material), 0.9)", "thick-material": "rgba(var(--color-thick-material), 0.9)",