updates
This commit is contained in:
parent
5c0034aabd
commit
9ad8d255de
70 changed files with 3498 additions and 2727 deletions
|
@ -1 +1,3 @@
|
|||
src-tauri
|
||||
src/components/ui
|
||||
dist
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
extends: ["prettier"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["prettier", "@typescript-eslint"],
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
rules: {
|
||||
"prettier/prettier": "error",
|
||||
},
|
||||
|
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
ignore-workspace-root-check=true
|
|
@ -1 +1,2 @@
|
|||
src-tauri
|
||||
src/components/ui
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
module.exports = {
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
useTabs: true,
|
||||
};
|
||||
|
|
31
.zed/scripts/eslint.cjs
Normal file
31
.zed/scripts/eslint.cjs
Normal file
|
@ -0,0 +1,31 @@
|
|||
const { ESLint } = require("eslint");
|
||||
|
||||
function readStdin() {
|
||||
return new Promise((resolve) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("readable", () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read())) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(async function () {
|
||||
const [file] = process.argv.slice(3);
|
||||
|
||||
const eslint = new ESLint({ fix: true });
|
||||
const code = await readStdin();
|
||||
|
||||
const results = await eslint.lintText(code, { filePath: file });
|
||||
|
||||
const fixedResult =
|
||||
results[0] && results[0].output ? results[0].output : code;
|
||||
|
||||
process.stdout.write(fixedResult);
|
||||
})();
|
39
.zed/settings.json
Normal file
39
.zed/settings.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"hard_tabs": true,
|
||||
|
||||
"language_overrides": {
|
||||
"TSX": {
|
||||
"format_on_save": "on",
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "./node_modules/.bin/eslint_d",
|
||||
"arguments": [
|
||||
"--stdin",
|
||||
"--fix-to-stdout",
|
||||
"--stdin-filename",
|
||||
"{buffer_path}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"format_on_save": "on",
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "./node_modules/.bin/eslint_d",
|
||||
"arguments": [
|
||||
"--stdin",
|
||||
"--fix-to-stdout",
|
||||
"--stdin-filename",
|
||||
"{buffer_path}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"CSS": {
|
||||
"hard_tabs": false,
|
||||
"tab_size": 2
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + Peact + TS</title>
|
||||
<title>Vybr: Music Player</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
35
package.json
35
package.json
|
@ -10,31 +10,36 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@preact/signals": "^1.2.2",
|
||||
"@preact/signals-core": "^1.5.1",
|
||||
"@tauri-apps/api": "^1.5.2",
|
||||
"@kobalte/core": "^0.12.1",
|
||||
"@popicons/react": "^0.0.9",
|
||||
"@solidjs/router": "^0.12.0",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"@vybr/player": "workspace:*",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
"preact": "^10.16.0",
|
||||
"tailwind-merge": "^2.2.0"
|
||||
"dayjs": "^1.11.10",
|
||||
"hugeicons-pro": "^0.0.20",
|
||||
"solid-icons": "^1.1.0",
|
||||
"solid-js": "^1.8.14",
|
||||
"tailwind-merge": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.5.0",
|
||||
"@tauri-apps/cli": "^1.5.8",
|
||||
"@types/node": "^20.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@types/node": "^20.11.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.2.1",
|
||||
"eslint_d": "^13.1.2",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0"
|
||||
"vite": "5.1.1",
|
||||
"vite-plugin-solid": "^2.9.1"
|
||||
}
|
||||
}
|
||||
|
|
3372
pnpm-lock.yaml
3372
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
packages:
|
||||
- "packages/*"
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
224
src-tauri/Cargo.lock
generated
224
src-tauri/Cargo.lock
generated
|
@ -136,9 +136,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
|
@ -194,9 +194,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
|||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.0"
|
||||
version = "1.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
|
@ -281,9 +281,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.15.6"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a"
|
||||
checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
|
@ -297,15 +297,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.31"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
||||
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -530,9 +530,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.3"
|
||||
version = "0.20.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
|
||||
checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
|
@ -540,9 +540,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.3"
|
||||
version = "0.20.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
|
||||
checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
|
@ -554,9 +554,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.3"
|
||||
version = "0.20.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||
checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
|
@ -653,7 +653,7 @@ dependencies = [
|
|||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.8",
|
||||
"toml 0.8.10",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
]
|
||||
|
@ -697,9 +697,9 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
|||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd"
|
||||
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
@ -1102,7 +1102,7 @@ dependencies = [
|
|||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.3",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
|
@ -1201,9 +1201,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
|||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||
checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
|
@ -1244,9 +1244,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.59"
|
||||
version = "0.1.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
|
@ -1301,7 +1301,7 @@ dependencies = [
|
|||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.3",
|
||||
"regex-automata 0.4.5",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
|
@ -1332,9 +1332,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
|
@ -1416,9 +1416,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.67"
|
||||
version = "0.3.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1"
|
||||
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
@ -1456,9 +1456,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.152"
|
||||
version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
|
@ -1466,7 +1466,7 @@ version = "0.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.2",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
@ -1482,9 +1482,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.12"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
|
||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
|
@ -1578,9 +1578,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"simd-adler32",
|
||||
|
@ -1637,10 +1637,16 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.17"
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
@ -1943,9 +1949,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
|
||||
checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
|
@ -1954,7 +1960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"indexmap 2.1.0",
|
||||
"indexmap 2.2.2",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
|
@ -2034,9 +2040,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.76"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -2168,13 +2174,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.2"
|
||||
version = "1.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.3",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
|
@ -2189,9 +2195,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.3"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -2227,11 +2233,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.30"
|
||||
version = "0.38.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
|
||||
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
@ -2308,18 +2314,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.195"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.195"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2328,9 +2334,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.111"
|
||||
version = "1.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
dependencies = [
|
||||
"itoa 1.0.10",
|
||||
"ryu",
|
||||
|
@ -2359,16 +2365,17 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.4.0"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
|
||||
checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.1.0",
|
||||
"indexmap 2.2.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
|
@ -2376,9 +2383,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.4.0"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
|
||||
checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
|
@ -2461,9 +2468,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.12.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2593d31f82ead8df961d8bd23a64c2ccf2eb5dd34b0a34bfb4dd54011c72009e"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
|
||||
[[package]]
|
||||
name = "soup2"
|
||||
|
@ -2581,18 +2588,18 @@ version = "6.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
|
||||
dependencies = [
|
||||
"cfg-expr 0.15.6",
|
||||
"cfg-expr 0.15.7",
|
||||
"heck 0.4.1",
|
||||
"pkg-config",
|
||||
"toml 0.8.8",
|
||||
"toml 0.8.10",
|
||||
"version-compare 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.16.5"
|
||||
version = "0.16.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75f5aefd6be4cd3ad3f047442242fd9f57cbfb3e565379f66b5e14749364fa4f"
|
||||
checksum = "d22205b267a679ca1c590b9f178488d50981fc3e48a1b91641ae31593db875ce"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cairo-rs",
|
||||
|
@ -2784,10 +2791,10 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#c5e8cd31ec86ba7ddcd524f1377a5bf09229fb9e"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#67405aed06f495c3ea42e0a62b789e2be48abb3a"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.2",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -2878,13 +2885,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.9.0"
|
||||
version = "3.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
|
||||
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
@ -2938,12 +2944,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.31"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
|
||||
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa 1.0.10",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
|
@ -2958,10 +2965,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
|||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
|
||||
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
|
@ -2982,9 +2990,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.35.1"
|
||||
version = "1.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
@ -3015,14 +3023,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.21.0",
|
||||
"toml_edit 0.22.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3040,7 +3048,7 @@ version = "0.19.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.1.0",
|
||||
"indexmap 2.2.2",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
|
@ -3049,11 +3057,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.0"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951"
|
||||
dependencies = [
|
||||
"indexmap 2.1.0",
|
||||
"indexmap 2.2.2",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
|
@ -3123,9 +3131,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "treediff"
|
||||
version = "4.0.2"
|
||||
version = "4.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303"
|
||||
checksum = "4d127780145176e2b5d16611cc25a900150e86e9fd79d3bde6ff3a37359c9cb5"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
@ -3138,9 +3146,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.14"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
|
||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
|
@ -3159,9 +3167,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
|
@ -3183,9 +3191,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
|
||||
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
|
||||
dependencies = [
|
||||
"getrandom 0.2.12",
|
||||
]
|
||||
|
@ -3258,9 +3266,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.90"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406"
|
||||
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
|
@ -3268,9 +3276,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.90"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd"
|
||||
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
|
@ -3283,9 +3291,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.90"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999"
|
||||
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
@ -3293,9 +3301,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.90"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7"
|
||||
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3306,9 +3314,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.90"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
|
||||
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
|
@ -3732,9 +3740,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.34"
|
||||
version = "0.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
|
||||
checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -3810,9 +3818,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.2.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "914566e6413e7fa959cc394fb30e563ba80f3541fbd40816d4c05a0fc3f2a0f1"
|
||||
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
|
|
@ -13,7 +13,18 @@ edition = "2021"
|
|||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.5", features = [ "macos-private-api", "window-minimize", "window-maximize", "window-hide", "window-unminimize", "window-unmaximize", "window-start-dragging", "window-close", "window-show", "shell-open"] }
|
||||
tauri = { version = "1.5", features = [
|
||||
"macos-private-api",
|
||||
"window-minimize",
|
||||
"window-maximize",
|
||||
"window-hide",
|
||||
"window-unminimize",
|
||||
"window-unmaximize",
|
||||
"window-start-dragging",
|
||||
"window-close",
|
||||
"window-show",
|
||||
"shell-open",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -6,6 +6,7 @@ use window_shadows::set_shadow;
|
|||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.invoke_handler(tauri::generate_handler![close_splashscreen])
|
||||
.setup(|app| {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
{
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "Vybr (MacOS)",
|
||||
"title": "Vybr",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"decorations": true,
|
||||
"hiddenTitle": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"visible": false
|
||||
|
@ -17,7 +16,9 @@
|
|||
"height": 400,
|
||||
"decorations": false,
|
||||
"url": "splashscreen.html",
|
||||
"label": "splashscreen"
|
||||
"label": "splashscreen",
|
||||
"resizable": false,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
"height": 400,
|
||||
"decorations": false,
|
||||
"url": "splashscreen.html",
|
||||
"label": "splashscreen"
|
||||
"label": "splashscreen",
|
||||
"resizable": false,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.preact:hover {
|
||||
filter: drop-shadow(0 0 2em #673ab8);
|
||||
}
|
40
src/App.tsx
40
src/App.tsx
|
@ -1,23 +1,41 @@
|
|||
import { isWindows } from "@tauri-apps/api/helpers/os-check";
|
||||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
|
||||
import Sidebar from "./components/sidebar";
|
||||
|
||||
import Player from "./components/player";
|
||||
import { UserProvider } from "./contexts/user";
|
||||
import { useBindings } from "./hooks/use-bindings";
|
||||
import { cn } from "./lib/utils";
|
||||
import { sizes } from "./lib/store";
|
||||
|
||||
import { dragging } from "./store";
|
||||
import TitleBar from "./components/title-bar";
|
||||
import { store } from "./contexts/player";
|
||||
|
||||
export default function App({ children }: RouteSectionProps) {
|
||||
useBindings();
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<UserProvider>
|
||||
<div class="grid h-[100dvh] grid-rows-[1fr,auto]">
|
||||
<div
|
||||
className={cn("select-none", {
|
||||
"cursor-col-resize": dragging.value.isDragging,
|
||||
})}
|
||||
class={cn(
|
||||
"grid h-full min-h-full grid-cols-[auto,1fr,auto] gap-2 bg-primary/90 p-2 pb-0 backdrop-blur",
|
||||
)}
|
||||
>
|
||||
{isWindows() && <TitleBar />}
|
||||
|
||||
<div className="grid grid-cols-[max-content,1fr]">
|
||||
<Sidebar />
|
||||
|
||||
<main class="space overflow-y-auto bg-secondary">{children}</main>
|
||||
|
||||
{store.queueOpen && (
|
||||
<div
|
||||
class="space bg-secondary"
|
||||
style={{ width: `${sizes.queue}px` }}
|
||||
>
|
||||
<h1 class="headline">Queue</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Player />
|
||||
</div>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="27.68" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 296"><path fill="#673AB8" d="m128 0l128 73.9v147.8l-128 73.9L0 221.7V73.9z"/><path fill="#FFF" d="M34.865 220.478c17.016 21.78 71.095 5.185 122.15-34.704c51.055-39.888 80.24-88.345 63.224-110.126c-17.017-21.78-71.095-5.184-122.15 34.704c-51.055 39.89-80.24 88.346-63.224 110.126Zm7.27-5.68c-5.644-7.222-3.178-21.402 7.573-39.253c11.322-18.797 30.541-39.548 54.06-57.923c23.52-18.375 48.303-32.004 69.281-38.442c19.922-6.113 34.277-5.075 39.92 2.148c5.644 7.223 3.178 21.403-7.573 39.254c-11.322 18.797-30.541 39.547-54.06 57.923c-23.52 18.375-48.304 32.004-69.281 38.441c-19.922 6.114-34.277 5.076-39.92-2.147Z"/><path fill="#FFF" d="M220.239 220.478c17.017-21.78-12.169-70.237-63.224-110.126C105.96 70.464 51.88 53.868 34.865 75.648c-17.017 21.78 12.169 70.238 63.224 110.126c51.055 39.889 105.133 56.485 122.15 34.704Zm-7.27-5.68c-5.643 7.224-19.998 8.262-39.92 2.148c-20.978-6.437-45.761-20.066-69.28-38.441c-23.52-18.376-42.74-39.126-54.06-57.923c-10.752-17.851-13.218-32.03-7.575-39.254c5.644-7.223 19.999-8.261 39.92-2.148c20.978 6.438 45.762 20.067 69.281 38.442c23.52 18.375 42.739 39.126 54.06 57.923c10.752 17.85 13.218 32.03 7.574 39.254Z"/><path fill="#FFF" d="M127.552 167.667c10.827 0 19.603-8.777 19.603-19.604c0-10.826-8.776-19.603-19.603-19.603c-10.827 0-19.604 8.777-19.604 19.603c0 10.827 8.777 19.604 19.604 19.604Z"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
229
src/components/player.tsx
Normal file
229
src/components/player.tsx
Normal file
|
@ -0,0 +1,229 @@
|
|||
import { As } from "@kobalte/core";
|
||||
import { ComponentProps, Match, Switch } from "solid-js";
|
||||
|
||||
import * as Icons from "@/icons";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
import { setStore, store } from "@/contexts/player";
|
||||
|
||||
import player from "@/lib/player";
|
||||
import { formatDuration } from "@/lib/duration";
|
||||
import { parseDuration } from "@/lib/time";
|
||||
import { A } from "@solidjs/router";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function VolumeIcon() {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={store.muted}>
|
||||
<Icons.VolumeOffStroke class="size-6" />
|
||||
</Match>
|
||||
<Match when={store.volume <= 0}>
|
||||
<Icons.VolumeMute02Stroke class="size-6" />
|
||||
</Match>
|
||||
<Match when={store.volume <= 0.33}>
|
||||
<Icons.VolumeMute01Stroke class="size-6" />
|
||||
</Match>
|
||||
<Match when={store.volume <= 0.66}>
|
||||
<Icons.VolumeLowStroke class="size-6" />
|
||||
</Match>
|
||||
<Match when={store.volume <= 1}>
|
||||
<Icons.VolumeHighStroke class="size-6" />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayPauseIcon() {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={store.playing}>
|
||||
<Icons.PauseStroke class="size-8 text-primary" />
|
||||
</Match>
|
||||
<Match when={!store.playing}>
|
||||
<Icons.PlayStroke class="size-8 text-primary" />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function RepeatIcon() {
|
||||
return (
|
||||
<Switch fallback={<Icons.RepeatStroke class="size-4 text-secondary" />}>
|
||||
<Match when={store.repeat === "all"}>
|
||||
<Icons.RepeatStroke class="size-4 text-primary" />
|
||||
</Match>
|
||||
<Match when={store.repeat === "one"}>
|
||||
<Icons.RepeatOne01Stroke class="size-4 text-primary" />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Player() {
|
||||
return (
|
||||
<div class="space grid h-24 grid-cols-3 gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="aspect-square h-full rounded-md border bg-secondary" />
|
||||
<div class="flex flex-1 flex-col gap-1">
|
||||
<h4>{store.track.title}</h4>
|
||||
<span class="text-sm text-secondary">
|
||||
{store.track.artists.map((artist, i) => (
|
||||
<>
|
||||
<A class="hover:underline" href={`/artists/${artist.id}`}>
|
||||
{artist.name}
|
||||
</A>
|
||||
{i < store.track.artists.length - 1 && ", "}
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
tooltip="Shuffle"
|
||||
onClick={() =>
|
||||
setStore((store) => ({ ...store, shuffle: !store.shuffle }))
|
||||
}
|
||||
class={cn({
|
||||
"bg-tertiary text-primary": store.shuffle,
|
||||
})}
|
||||
>
|
||||
<Icons.ShuffleStroke class="size-4" />
|
||||
</Button>
|
||||
|
||||
<Button tooltip="Previous">
|
||||
<Icons.PreviousStroke class="size-6" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
tooltip={store.playing ? "Pause" : "Play"}
|
||||
onClick={() => player.playPause()}
|
||||
>
|
||||
<PlayPauseIcon />
|
||||
</Button>
|
||||
|
||||
<Button tooltip="Next">
|
||||
<Icons.NextStroke class="size-6" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
tooltip="Repeat"
|
||||
onClick={() =>
|
||||
setStore((store) => ({
|
||||
...store,
|
||||
repeat:
|
||||
store.repeat === "none"
|
||||
? "all"
|
||||
: store.repeat === "all"
|
||||
? "one"
|
||||
: "none",
|
||||
}))
|
||||
}
|
||||
class={cn({
|
||||
"bg-tertiary text-primary":
|
||||
store.repeat === "all" || store.repeat === "one",
|
||||
})}
|
||||
>
|
||||
<RepeatIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full items-center gap-4 text-sm">
|
||||
<span class="w-16">
|
||||
{formatDuration(parseDuration(store.progress))}
|
||||
</span>
|
||||
<div
|
||||
class="h-1.5 w-full cursor-pointer overflow-hidden rounded-md bg-tertiary"
|
||||
onClick={(event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const width = rect.width;
|
||||
const progress = x / width;
|
||||
|
||||
player.seek(progress * store.duration);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="h-full w-0 rounded-md bg-accent"
|
||||
style={{ width: `${(store.progress / store.duration) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span class="w-16 text-right">
|
||||
{formatDuration(parseDuration(store.duration))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<Button tooltip="Sound output">
|
||||
<Icons.RadioStroke class="size-6" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
tooltip="Queue"
|
||||
onClick={() => {
|
||||
setStore((store) => ({ ...store, queueOpen: !store.queueOpen }));
|
||||
}}
|
||||
>
|
||||
<Icons.Queue02Stroke class="size-6" />
|
||||
</Button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
tooltip="Volume"
|
||||
onClick={() =>
|
||||
setStore((store) => ({ ...store, muted: !store.muted }))
|
||||
}
|
||||
>
|
||||
<VolumeIcon />
|
||||
</Button>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
class="block h-full max-w-24 accent-accent"
|
||||
value={store.muted ? 0 : store.volume}
|
||||
onInput={(event) => {
|
||||
setStore((store) => ({
|
||||
...store,
|
||||
volume: Number(event.currentTarget.value),
|
||||
muted: false,
|
||||
}));
|
||||
}}
|
||||
min={0}
|
||||
step={0.01}
|
||||
max={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ButtonProps extends ComponentProps<"button"> {
|
||||
tooltip?: string;
|
||||
children: any;
|
||||
|
||||
onClick?: () => void;
|
||||
}
|
||||
function Button(props: ButtonProps) {
|
||||
return (
|
||||
<Tooltip placement="top">
|
||||
<TooltipTrigger asChild>
|
||||
<As
|
||||
component="button"
|
||||
class={cn(
|
||||
"flex size-8 cursor-pointer items-center justify-center rounded-md text-secondary transition-colors hover:scale-105 hover:text-primary",
|
||||
props.class,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</As>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{props.tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import { useCallback, useEffect } from "preact/hooks";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { dragging } from "@/store";
|
||||
|
||||
const { isDragging } = dragging.value;
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside
|
||||
className="relative flex h-[100dvh] w-full bg-secondary px-2 py-8"
|
||||
style={{ width: dragging.value.width }}
|
||||
>
|
||||
<div className=""></div>
|
||||
<nav className="">
|
||||
<a href="#">Lol</a>
|
||||
</nav>
|
||||
<Divider />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
function onMouseDown() {
|
||||
dragging.value = {
|
||||
...dragging.value,
|
||||
isDragging: true,
|
||||
};
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
dragging.value = {
|
||||
...dragging.value,
|
||||
isDragging: false,
|
||||
};
|
||||
}
|
||||
|
||||
const handler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (dragging.value.isDragging) {
|
||||
const width = Math.min(Math.max(200, event.clientX), 320);
|
||||
dragging.value = {
|
||||
...dragging.value,
|
||||
width,
|
||||
};
|
||||
localStorage.setItem("sidebar-width", width.toString());
|
||||
}
|
||||
},
|
||||
[dragging.value.isDragging],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("mousemove", handler);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handler);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group absolute -right-2 bottom-0 top-0 px-2 hover:cursor-col-resize",
|
||||
{},
|
||||
)}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
>
|
||||
<div className="h-full w-[1px] bg-border transition-colors group-hover:scale-x-[2] group-hover:bg-accent/50"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
112
src/components/sidebar/index.tsx
Normal file
112
src/components/sidebar/index.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { As } from "@kobalte/core";
|
||||
import { A } from "@solidjs/router";
|
||||
import { createSignal, useContext } from "solid-js";
|
||||
|
||||
import * as Icons from "@/icons";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import Playlists from "./playlists";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { UserContext } from "@/contexts/user";
|
||||
import { sizes } from "@/lib/store";
|
||||
|
||||
const links = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Library",
|
||||
icon: Icons.FolderLibraryStroke,
|
||||
},
|
||||
{
|
||||
href: "/search",
|
||||
name: "Search",
|
||||
icon: Icons.FileSearchStroke,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
return (
|
||||
<aside
|
||||
class="relative grid h-full w-full grid-rows-[max-content,1fr] flex-col gap-2 text-secondary"
|
||||
style={{ width: `${sizes.sidebar}px` }}
|
||||
>
|
||||
<div class="space flex items-center justify-between bg-secondary py-4">
|
||||
<A
|
||||
href={`/users/${user.id}`}
|
||||
class="flex h-full items-center gap-4 rounded-md px-3 transition-colors hover:bg-tertiary"
|
||||
>
|
||||
<div class="flex size-8 items-center justify-center rounded-full bg-tertiary text-sm font-bold text-secondary">
|
||||
{user.initials}
|
||||
</div>
|
||||
<span class="text-primary">{user.name}</span>
|
||||
</A>
|
||||
|
||||
<DropdownMenu placement="bottom-end" onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger class="transition-colors hover:text-primary">
|
||||
<Icons.ArrowDown01Stroke
|
||||
class={cn("size-6 transition-transform", {
|
||||
"rotate-180 transform-gpu": open(),
|
||||
})}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem asChild>
|
||||
<As component={A} href="/users/asd">
|
||||
Profile
|
||||
</As>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<As component={A} href="/settings">
|
||||
Settings
|
||||
</As>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
class="text-red-400"
|
||||
onClick={() => alert("Would logout")}
|
||||
>
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="space h-100 flex flex-col gap-8 bg-secondary">
|
||||
<nav>
|
||||
{links.map((link) => (
|
||||
<A
|
||||
href={link.href}
|
||||
activeClass="bg-tertiary text-accent"
|
||||
class="mb-2 flex h-10 items-center gap-4 rounded-md px-4 transition-colors hover:bg-tertiary"
|
||||
end
|
||||
>
|
||||
<link.icon class="size-5" />
|
||||
<span class="text-primary">{link.name}</span>
|
||||
</A>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<nav>
|
||||
<header class="mb-2 flex items-center justify-between">
|
||||
<h4 class="text-sm font-light uppercase tracking-widest text-secondary">
|
||||
Playlists
|
||||
</h4>
|
||||
<button>
|
||||
<Icons.PlusSignStroke size={20} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<Playlists />
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
34
src/components/sidebar/playlists.tsx
Normal file
34
src/components/sidebar/playlists.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { A } from "@solidjs/router";
|
||||
import { For, Show, createResource } from "solid-js";
|
||||
|
||||
import * as Icons from "@/icons";
|
||||
import { getPlaylists } from "@/lib/playlists";
|
||||
|
||||
export default function Playlists() {
|
||||
const [playlists] = createResource(getPlaylists);
|
||||
|
||||
return (
|
||||
<Show when={!playlists.loading} fallback={<p>Loading...</p>}>
|
||||
<For each={playlists.latest}>
|
||||
{(playlist) => {
|
||||
const Icon =
|
||||
playlist.playlist_type === "folder"
|
||||
? Icons.Folder01Stroke
|
||||
: Icons.Playlist03Stroke;
|
||||
|
||||
return (
|
||||
<A
|
||||
href={`/${playlist.playlist_type}s/${playlist.id}`}
|
||||
activeClass="bg-tertiary text-accent"
|
||||
class="mb-2 flex h-10 items-center gap-4 rounded-md px-4 transition-colors hover:bg-tertiary"
|
||||
end
|
||||
>
|
||||
<Icon class="size-5" />
|
||||
<span class="text-primary">{playlist.name}</span>
|
||||
</A>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
);
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
import { isWindows } from "@tauri-apps/api/helpers/os-check";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
|
||||
export default function TitleBar() {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-8 w-full select-none border-b bg-primary"
|
||||
>
|
||||
{isWindows() && <Windows />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Windows() {
|
||||
return (
|
||||
<div className="ml-auto flex">
|
||||
<button
|
||||
className="flex w-12 items-center justify-center transition-colors hover:bg-tertiary"
|
||||
onClick={() => appWindow.minimize()}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20 12L4 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="flex w-12 items-center justify-center transition-colors hover:bg-tertiary"
|
||||
onClick={() => appWindow.toggleMaximize()}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 12C2.5 7.52166 2.5 5.28249 3.89124 3.89124C5.28249 2.5 7.52166 2.5 12 2.5C16.4783 2.5 18.7175 2.5 20.1088 3.89124C21.5 5.28249 21.5 7.52166 21.5 12C21.5 16.4783 21.5 18.7175 20.1088 20.1088C18.7175 21.5 16.4783 21.5 12 21.5C7.52166 21.5 5.28249 21.5 3.89124 20.1088C2.5 18.7175 2.5 16.4783 2.5 12Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="flex w-12 items-center justify-center transition-colors hover:bg-red-500"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 5L5 19M5 5L19 19"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
184
src/components/ui/dropdown-menu.tsx
Normal file
184
src/components/ui/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,184 @@
|
|||
import type { Component, ComponentProps } from "solid-js"
|
||||
import { splitProps } from "solid-js"
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core"
|
||||
import { TbCheck, TbChevronRight, TbCircle } from "solid-icons/tb"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu: Component<DropdownMenuPrimitive.DropdownMenuRootProps> = (props) => {
|
||||
return <DropdownMenuPrimitive.Root gutter={4} {...props} />
|
||||
}
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuContent: Component<DropdownMenuPrimitive.DropdownMenuContentProps> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
class={cn(
|
||||
"bg-secondary text-primary animate-content-hide data-[expanded]:animate-content-show z-50 min-w-[12rem] origin-[var(--kb-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuItem: Component<DropdownMenuPrimitive.DropdownMenuItemProps> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
"cursor-pointer focus:bg-tertiary focus:text-accent-foreground relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuShortcut: Component<ComponentProps<"span">> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return <span class={cn("ml-auto text-xs tracking-widest opacity-60", props.class)} {...rest} />
|
||||
}
|
||||
|
||||
const DropdownMenuLabel: Component<ComponentProps<"div"> & { inset?: boolean }> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class", "inset"])
|
||||
return (
|
||||
<div
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", props.inset && "pl-8", props.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuSeparator: Component<DropdownMenuPrimitive.DropdownMenuSeparatorProps> = (
|
||||
props
|
||||
) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
class={cn("bg-muted -mx-1 my-1 h-px", props.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuSubTrigger: Component<DropdownMenuPrimitive.DropdownMenuSubTriggerProps> = (
|
||||
props
|
||||
) => {
|
||||
const [, rest] = splitProps(props, ["class", "children"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{props.children}
|
||||
<TbChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuSubContent: Component<DropdownMenuPrimitive.DropdownMenuSubContentProps> = (
|
||||
props
|
||||
) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground animate-in z-50 min-w-[8rem] origin-[var(--kb-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuCheckboxItem: Component<DropdownMenuPrimitive.DropdownMenuCheckboxItemProps> = (
|
||||
props
|
||||
) => {
|
||||
const [, rest] = splitProps(props, ["class", "children"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<TbCheck class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuGroupLabel: Component<DropdownMenuPrimitive.DropdownMenuGroupLabelProps> = (
|
||||
props
|
||||
) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.GroupLabel
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", props.class)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuRadioItem: Component<DropdownMenuPrimitive.DropdownMenuRadioItemProps> = (
|
||||
props
|
||||
) => {
|
||||
const [, rest] = splitProps(props, ["class", "children"])
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<TbCircle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuGroupLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem
|
||||
}
|
28
src/components/ui/tooltip.tsx
Normal file
28
src/components/ui/tooltip.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { splitProps, type Component } from "solid-js"
|
||||
|
||||
import { Tooltip as TooltipPrimitive } from "@kobalte/core"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tooltip: Component<TooltipPrimitive.TooltipRootProps> = (props) => {
|
||||
return <TooltipPrimitive.Root gutter={4} {...props} />
|
||||
}
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent: Component<TooltipPrimitive.TooltipContentProps> = (props) => {
|
||||
const [, rest] = splitProps(props, ["class"])
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
class={cn(
|
||||
"bg-primary text-primary animate-in fade-in-0 zoom-in-95 z-50 origin-[var(--kb-popover-content-transform-origin)] overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
|
||||
props.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent }
|
76
src/contexts/player.tsx
Normal file
76
src/contexts/player.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
|
||||
import player from "@/lib/player";
|
||||
import type { Track } from "@/types/track";
|
||||
|
||||
interface PlayerStore {
|
||||
playing: boolean;
|
||||
progress: number;
|
||||
duration: number;
|
||||
|
||||
muted: boolean;
|
||||
volume: number;
|
||||
|
||||
track: Track;
|
||||
|
||||
queueOpen: boolean;
|
||||
|
||||
shuffle: boolean;
|
||||
repeat: "none" | "one" | "all";
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore<PlayerStore>({
|
||||
playing: false,
|
||||
progress: 0,
|
||||
duration: 0,
|
||||
|
||||
muted: false,
|
||||
volume: 0.5,
|
||||
|
||||
track: player.getTrack(),
|
||||
|
||||
queueOpen: false,
|
||||
|
||||
shuffle: false,
|
||||
repeat: "none",
|
||||
});
|
||||
|
||||
const audio = player.getAudio();
|
||||
|
||||
audio.addEventListener("play", () =>
|
||||
setStore((store) => ({ ...store, playing: true })),
|
||||
);
|
||||
|
||||
audio.addEventListener("pause", () =>
|
||||
setStore((store) => ({ ...store, playing: false })),
|
||||
);
|
||||
|
||||
audio.addEventListener("ended", () =>
|
||||
setStore((store) => ({ ...store, playing: false })),
|
||||
);
|
||||
|
||||
audio.addEventListener("timeupdate", () => {
|
||||
setStore((store) => ({
|
||||
...store,
|
||||
progress: Math.floor(audio.currentTime),
|
||||
duration: Math.floor(audio.duration),
|
||||
}));
|
||||
});
|
||||
|
||||
audio.addEventListener("volumechange", () => {
|
||||
setStore((store) => ({
|
||||
...store,
|
||||
muted: audio.muted,
|
||||
volume: audio.volume,
|
||||
}));
|
||||
});
|
||||
|
||||
player.on("track", (track) => {
|
||||
setStore((store) => ({
|
||||
...store,
|
||||
track,
|
||||
progress: 0,
|
||||
}));
|
||||
});
|
||||
|
||||
export { store, setStore };
|
46
src/contexts/user.tsx
Normal file
46
src/contexts/user.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { fetchAPI } from "@/lib/api";
|
||||
import { Show } from "solid-js";
|
||||
import { createContext, createResource, JSX } from "solid-js";
|
||||
|
||||
const DEFAULT_USER = {
|
||||
id: "",
|
||||
name: "Miguel da Mota",
|
||||
email: "miguel@damota.de",
|
||||
initials: "",
|
||||
};
|
||||
|
||||
export const UserContext = createContext({
|
||||
user: DEFAULT_USER,
|
||||
});
|
||||
|
||||
export function UserProvider(props: { children: JSX.Element }) {
|
||||
const [user] = createResource(getMe);
|
||||
|
||||
return (
|
||||
<Show when={!user.loading} fallback={<p>Loading...</p>}>
|
||||
<UserContext.Provider value={{ user: user()! }}>
|
||||
{props.children}
|
||||
</UserContext.Provider>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function getInitials(name: string) {
|
||||
const [first, ...others] = name.split(" ");
|
||||
|
||||
let initials = first[0];
|
||||
|
||||
if (others.length > 0) {
|
||||
initials += others[0][0];
|
||||
}
|
||||
|
||||
return initials.toUpperCase();
|
||||
}
|
||||
|
||||
async function getMe(): Promise<typeof DEFAULT_USER> {
|
||||
const user = await fetchAPI<typeof DEFAULT_USER>("/me");
|
||||
|
||||
user.initials = getInitials(user.name);
|
||||
|
||||
return user;
|
||||
}
|
16
src/hooks/use-bindings.ts
Normal file
16
src/hooks/use-bindings.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
const bindings = new Map<string, (event: KeyboardEvent) => void>();
|
||||
|
||||
export function useBindings() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (bindings.has(event.key)) {
|
||||
event.preventDefault();
|
||||
bindings.get(event.key)?.(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function bind(key: string, callback: (event: KeyboardEvent) => void) {
|
||||
bindings.set(key, callback);
|
||||
}
|
||||
|
||||
bind("a", () => console.log("a was pressed"));
|
56
src/index.css
Normal file
56
src/index.css
Normal file
|
@ -0,0 +1,56 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
font-weight: 400;
|
||||
src: url("/assets/fonts/Rubik-Regular.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
font-weight: 700;
|
||||
src: url("/assets/fonts/Rubik-Bold.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
font-weight: 900;
|
||||
src: url("/assets/fonts/Rubik-Black.woff2");
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Rubik";
|
||||
overscroll-behavior-y: none;
|
||||
|
||||
height: 100%;
|
||||
line-height: normal;
|
||||
min-height: 600px;
|
||||
min-width: 800px;
|
||||
|
||||
@apply bg-primary text-primary relative select-none;
|
||||
}
|
||||
|
||||
.space {
|
||||
@apply rounded-lg p-4;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-0 border-t;
|
||||
}
|
||||
|
||||
.playlist-row {
|
||||
@apply grid grid-cols-[48px,1fr,.5fr,128px,48px] items-center gap-4 rounded-md px-4 py-2 md:grid-cols-[48px,1fr,.5fr,128px,128px,48px];
|
||||
}
|
||||
|
||||
/* Custom classes */
|
||||
.headline {
|
||||
@apply text-3xl font-bold;
|
||||
}
|
25
src/lib/api.ts
Normal file
25
src/lib/api.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
const API_URL = "http://localhost:9000";
|
||||
|
||||
const ACCESS_TOKEN =
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3Mzg0MjY1MDMsInVzZXJfaWQiOiI1a3hERGNvSFpTQkM3dUVZc20zRnZrdEsifQ.Z3xceSQKjDg5nHQel3jGlUDjWiCjDUmhFgJtrirxyew";
|
||||
|
||||
export async function fetchAPI<Body>(
|
||||
url: string,
|
||||
options?: RequestInit,
|
||||
): Promise<Body> {
|
||||
const response = await fetch(`${API_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
authorization: `Bearer ${ACCESS_TOKEN}`,
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
return body;
|
||||
}
|
15
src/lib/duration.ts
Normal file
15
src/lib/duration.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { ParseDuration } from "./time";
|
||||
|
||||
export function formatDuration({ hours, minutes, seconds }: ParseDuration) {
|
||||
let output = "";
|
||||
|
||||
if (hours) {
|
||||
output += hours.toString().padStart(2, "0") + ":";
|
||||
}
|
||||
|
||||
output += minutes.toString().padStart(2, "0") + ":";
|
||||
|
||||
output += seconds.toString().padStart(2, "0");
|
||||
|
||||
return output;
|
||||
}
|
21
src/lib/player/chunk-cache.ts
Normal file
21
src/lib/player/chunk-cache.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Chunk from "./chunk";
|
||||
|
||||
class ChunkCache {
|
||||
private cache = new Map<number, ArrayBuffer>();
|
||||
|
||||
get(index: number) {
|
||||
return this.cache.get(index);
|
||||
}
|
||||
|
||||
set(chunk: Chunk) {
|
||||
this.cache.set(chunk.getIndex(), chunk.getBuffer());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const chunkCache = new ChunkCache();
|
||||
|
||||
export default chunkCache;
|
73
src/lib/player/chunk-queue.ts
Normal file
73
src/lib/player/chunk-queue.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { Player } from ".";
|
||||
|
||||
import Chunk from "./chunk";
|
||||
import chunkCache from "./chunk-cache";
|
||||
|
||||
export default class ChunkQueue {
|
||||
private queue = new Map<number, ArrayBuffer>();
|
||||
|
||||
private index = -1;
|
||||
|
||||
constructor(private readonly player: Player) {
|
||||
this.index = player.currentChunk;
|
||||
}
|
||||
|
||||
next() {
|
||||
const next = this.queue.get(this.index);
|
||||
|
||||
if (!next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delete entry from queue.
|
||||
this.queue.delete(this.index);
|
||||
|
||||
// Increment index.
|
||||
this.index++;
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
enqueue(chunk: Chunk) {
|
||||
this.queue.set(chunk.getIndex(), chunk.getBuffer());
|
||||
|
||||
this.player.emit("queue");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear
|
||||
*/
|
||||
clear() {
|
||||
this.queue.clear();
|
||||
}
|
||||
|
||||
setIndex(index: number) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.queue.size;
|
||||
}
|
||||
|
||||
private async getChunk(index: number) {
|
||||
const cached = chunkCache.get(index);
|
||||
|
||||
if (cached) {
|
||||
return new Chunk(cached, index);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
this.player
|
||||
.getPlaybackInfo()
|
||||
.stream_url.replace("{chunk}", String(index))!,
|
||||
);
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new Chunk(buffer, index);
|
||||
}
|
||||
|
||||
async load(index: number) {
|
||||
const chunk = await this.getChunk(index);
|
||||
this.enqueue(chunk);
|
||||
}
|
||||
}
|
14
src/lib/player/chunk.ts
Normal file
14
src/lib/player/chunk.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default class Chunk {
|
||||
constructor(
|
||||
private readonly _buffer: ArrayBuffer,
|
||||
private readonly _index: number,
|
||||
) {}
|
||||
|
||||
getIndex() {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
getBuffer() {
|
||||
return this._buffer;
|
||||
}
|
||||
}
|
319
src/lib/player/index.ts
Normal file
319
src/lib/player/index.ts
Normal file
|
@ -0,0 +1,319 @@
|
|||
import ChunkQueue from "./chunk-queue";
|
||||
import type { Events, EventListeners } from "./types/player";
|
||||
|
||||
import { DEFAULT_PLAYBACK_INFO, PlaybackInfo } from "./types/playback-info";
|
||||
import { DEFAULT_TRACK, type Track } from "@/types/track";
|
||||
import chunkCache from "./chunk-cache";
|
||||
|
||||
const CHUNK_LENGTH = 5,
|
||||
PRELOAD_CHUNKS = 3;
|
||||
|
||||
export class Player {
|
||||
static audio = new Audio();
|
||||
|
||||
private _listeners = new Map<Events, EventListeners[Events][]>();
|
||||
private _initialized = false;
|
||||
|
||||
private track = DEFAULT_TRACK;
|
||||
|
||||
private queue = new ChunkQueue(this);
|
||||
|
||||
private playbackInfo: PlaybackInfo = DEFAULT_PLAYBACK_INFO;
|
||||
// private state
|
||||
|
||||
private sourceBuffer: SourceBuffer | null = null;
|
||||
|
||||
currentChunk = 0;
|
||||
|
||||
constructor() {
|
||||
navigator.mediaSession.setActionHandler("pause", () => this.playPause());
|
||||
navigator.mediaSession.setActionHandler("play", () => this.playPause());
|
||||
|
||||
this.createMediaSource = this.createMediaSource.bind(this);
|
||||
this.sourceOpen = this.sourceOpen.bind(this);
|
||||
|
||||
// this.startStateUpdate = this.startStateUpdate.bind(this);
|
||||
// this.updateState = this.updateState.bind(this);
|
||||
|
||||
this.reset = this.reset.bind(this);
|
||||
this.play = this.play.bind(this);
|
||||
this.playPause = this.playPause.bind(this);
|
||||
this.runQueue = this.runQueue.bind(this);
|
||||
|
||||
this.changeTrack = this.changeTrack.bind(this);
|
||||
this.processNextChunk = this.processNextChunk.bind(this);
|
||||
|
||||
this.registerEvents = this.registerEvents.bind(this);
|
||||
|
||||
this.registerEvents();
|
||||
}
|
||||
|
||||
private registerEvents() {
|
||||
this.on("queue", this.runQueue);
|
||||
}
|
||||
|
||||
getAudio() {
|
||||
return Player.audio;
|
||||
}
|
||||
|
||||
getTrack(): Track {
|
||||
return this.track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change track.
|
||||
*/
|
||||
async changeTrack(track: Track, progress = 0) {
|
||||
// If track is the same, just play/pause
|
||||
if (this.track.id === track.id && !progress) {
|
||||
this.playPause();
|
||||
return;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
|
||||
Player.audio.pause();
|
||||
|
||||
if (this.track.id !== track.id) {
|
||||
this.queue.clear();
|
||||
}
|
||||
|
||||
this.track = track;
|
||||
await this.fetchPlaybackInfo();
|
||||
|
||||
this.emit("track", track);
|
||||
|
||||
// Update mediaSession
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: track.title,
|
||||
artist: track.artists.map((artist) => artist.name).join(", "),
|
||||
album: "Unknown",
|
||||
// artwork: [
|
||||
// {
|
||||
// src: track.image,
|
||||
// sizes: "1280x1280",
|
||||
// type: "image/jpg",
|
||||
// },
|
||||
// ],
|
||||
});
|
||||
}
|
||||
|
||||
this.queue = new ChunkQueue(this);
|
||||
|
||||
this._progress = progress;
|
||||
this.currentChunk = getChunkFromTime(progress);
|
||||
|
||||
this.play();
|
||||
}
|
||||
private _progress = 0;
|
||||
|
||||
private _checkedSecond = 0;
|
||||
private processNextChunk() {
|
||||
const second = Math.floor(Player.audio.currentTime);
|
||||
|
||||
if (second % CHUNK_LENGTH !== 0 || this._checkedSecond === second) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkedSecond = second;
|
||||
|
||||
const nextChunk = this.currentChunk + 1;
|
||||
this.queue.setIndex(nextChunk);
|
||||
|
||||
if (nextChunk > this.playbackInfo.max_chunks) {
|
||||
this.sourceBuffer?.removeEventListener("updateend", this.runQueue);
|
||||
return;
|
||||
}
|
||||
|
||||
this.queue.load(nextChunk);
|
||||
|
||||
this.currentChunk = nextChunk;
|
||||
}
|
||||
|
||||
private async fetchPlaybackInfo() {
|
||||
const { id } = this.track;
|
||||
|
||||
const data = await fetch(`http://localhost:9001/tracks/${id}`).then((res) =>
|
||||
res.json(),
|
||||
);
|
||||
|
||||
this.playbackInfo = data;
|
||||
}
|
||||
|
||||
private createMediaSource() {
|
||||
const mediaSource = new MediaSource();
|
||||
|
||||
const audio = this.getAudio();
|
||||
audio.src = URL.createObjectURL(mediaSource);
|
||||
|
||||
audio.currentTime = this._progress;
|
||||
|
||||
mediaSource.addEventListener("sourceopen", () => {
|
||||
this.sourceOpen(mediaSource);
|
||||
});
|
||||
}
|
||||
|
||||
private async sourceOpen(mediaSource: MediaSource) {
|
||||
const { queue } = this;
|
||||
queue.clear();
|
||||
chunkCache.clear();
|
||||
|
||||
this.sourceBuffer = mediaSource.addSourceBuffer(this.getSourceBufferType());
|
||||
|
||||
--this.currentChunk;
|
||||
|
||||
this.queue.setIndex(0);
|
||||
await this.queue.load(0);
|
||||
|
||||
this.queue.setIndex(++this.currentChunk);
|
||||
|
||||
// Prleoad x chunks.
|
||||
for (let i = 0; i < PRELOAD_CHUNKS; i++) {
|
||||
if (this.currentChunk > this.playbackInfo.max_chunks) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Fetch chunk.
|
||||
this.queue.load(this.currentChunk++);
|
||||
}
|
||||
|
||||
--this.currentChunk;
|
||||
|
||||
this.sourceBuffer.addEventListener("updateend", this.runQueue);
|
||||
|
||||
Player.audio.addEventListener("timeupdate", this.processNextChunk);
|
||||
}
|
||||
|
||||
private getSourceBufferType(): string {
|
||||
const types = ["audio/mp4", 'audio/mp4; codecs="flac"'];
|
||||
|
||||
for (const type of types) {
|
||||
if (MediaSource.isTypeSupported(type)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private runQueue() {
|
||||
const { queue } = this;
|
||||
|
||||
if (queue.size === 0 || this.sourceBuffer?.updating) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = queue.next();
|
||||
|
||||
if (!buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.sourceBuffer?.appendBuffer(buffer);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private play() {
|
||||
if (!this._initialized) {
|
||||
this.createMediaSource();
|
||||
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
Player.audio.play();
|
||||
}
|
||||
|
||||
playPause() {
|
||||
if (!this.track.id) return;
|
||||
|
||||
const audio = this.getAudio();
|
||||
|
||||
if (audio.paused) {
|
||||
this.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this._initialized = false;
|
||||
|
||||
if (this.sourceBuffer) {
|
||||
this.sourceBuffer.removeEventListener("updateend", this.runQueue);
|
||||
}
|
||||
|
||||
Player.audio.removeEventListener("timeupdate", this.processNextChunk);
|
||||
}
|
||||
|
||||
public getPlaybackInfo() {
|
||||
return this.playbackInfo;
|
||||
}
|
||||
|
||||
/// Controls
|
||||
get volume() {
|
||||
return Player.audio.volume;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
Player.audio.volume = value;
|
||||
}
|
||||
|
||||
get muted() {
|
||||
return Player.audio.muted;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
Player.audio.muted = value;
|
||||
}
|
||||
|
||||
get currentTime() {
|
||||
return Player.audio.currentTime;
|
||||
}
|
||||
|
||||
isPlaying() {
|
||||
return !Player.audio.paused;
|
||||
}
|
||||
|
||||
seek(progress: number) {
|
||||
this.changeTrack(this.track, progress);
|
||||
}
|
||||
|
||||
/// Events
|
||||
public emit<E extends Events>(event: E, ...args: unknown[]) {
|
||||
const listeners: EventListeners[Events][] =
|
||||
this._listeners.get(event) ?? [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
listeners.forEach((listener) => listener(...args));
|
||||
}
|
||||
|
||||
public on<E extends Events>(event: E, listener: EventListeners[E]) {
|
||||
const eventListeners = this._listeners.get(event) ?? [];
|
||||
|
||||
eventListeners.push(listener);
|
||||
|
||||
this._listeners.set(event, eventListeners);
|
||||
}
|
||||
|
||||
public off<E extends Events>(event: E, listener: EventListeners[E]) {
|
||||
const eventListeners = this._listeners.get(event) ?? [];
|
||||
|
||||
this._listeners.set(
|
||||
event,
|
||||
eventListeners.filter((eventListener) => eventListener !== listener),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const player = new Player();
|
||||
|
||||
export default player;
|
||||
|
||||
function getChunkFromTime(time: number) {
|
||||
return Math.floor(time / CHUNK_LENGTH);
|
||||
}
|
13
src/lib/player/types/playback-info.ts
Normal file
13
src/lib/player/types/playback-info.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export interface PlaybackInfo {
|
||||
id: string;
|
||||
duration: number;
|
||||
max_chunks: number;
|
||||
stream_url: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_PLAYBACK_INFO: PlaybackInfo = {
|
||||
id: "",
|
||||
duration: 0,
|
||||
max_chunks: 0,
|
||||
stream_url: "",
|
||||
};
|
26
src/lib/player/types/player.ts
Normal file
26
src/lib/player/types/player.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { Track } from "@/types/track";
|
||||
|
||||
export interface PlayerState {
|
||||
playing: boolean;
|
||||
loading: boolean;
|
||||
|
||||
volume: number;
|
||||
progress: number;
|
||||
track: Track;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export interface EventListeners extends Record<Events, Function> {
|
||||
track: (track: Track) => void;
|
||||
queue: (chunk: number) => void;
|
||||
state: (state: PlayerState) => void;
|
||||
progress: (progress: number) => void;
|
||||
}
|
||||
|
||||
export type Events =
|
||||
| "track"
|
||||
| "queue"
|
||||
| "state"
|
||||
| "progress"
|
||||
| "play"
|
||||
| "pause";
|
|
@ -1,8 +1,46 @@
|
|||
interface Playlist {
|
||||
id: string;
|
||||
name: string;
|
||||
import { Playlist } from "@/types/playlist";
|
||||
import { fetchAPI } from "./api";
|
||||
|
||||
const playlists = new Map<string, Playlist>();
|
||||
|
||||
export async function getPlaylist(playlistId: string): Promise<Playlist> {
|
||||
if (playlists.has(playlistId)) {
|
||||
return playlists.get(playlistId)!;
|
||||
}
|
||||
|
||||
export function getPlaylists(): Playlist[] {
|
||||
return [];
|
||||
const playlist = await fetchAPI<Playlist>(`/playlists/${playlistId}`);
|
||||
|
||||
playlists.set(playlistId, playlist);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
export async function getPlaylists(): Promise<Playlist[]> {
|
||||
return fetchAPI("/me/playlists");
|
||||
}
|
||||
|
||||
export function parseDuration(seconds: string | number) {
|
||||
const hours = Math.floor(Number(seconds) / 3600);
|
||||
seconds = Number(seconds) % 3600;
|
||||
|
||||
let minutes: string | number = Math.floor(seconds / 60);
|
||||
seconds = seconds % 60;
|
||||
|
||||
let output = "";
|
||||
|
||||
if (hours > 0) {
|
||||
output += `${hours}h `;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
if (minutes < 10 && hours > 0) minutes = `0${minutes}`;
|
||||
|
||||
output += `${minutes}m `;
|
||||
}
|
||||
|
||||
if (seconds < 10) seconds = `0${seconds}`;
|
||||
|
||||
output += `${seconds}s`;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
|
6
src/lib/store.ts
Normal file
6
src/lib/store.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
|
||||
export const [sizes, setSizes] = createStore({
|
||||
sidebar: 320,
|
||||
queue: 320,
|
||||
});
|
23
src/lib/time.ts
Normal file
23
src/lib/time.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import time from "dayjs";
|
||||
|
||||
import duration from "dayjs/plugin/duration";
|
||||
|
||||
time.extend(duration);
|
||||
|
||||
export default time;
|
||||
|
||||
export interface ParseDuration {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
export function parseDuration(seconds: number): ParseDuration {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
seconds = seconds % 3600;
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
seconds = seconds % 60;
|
||||
|
||||
return { hours, minutes, seconds };
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { ClassValue } from "clsx"
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
36
src/main.tsx
36
src/main.tsx
|
@ -1,13 +1,35 @@
|
|||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { render } from "preact";
|
||||
/* @refresh reload */
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
import { lazy } from "solid-js";
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
import "./styles.css";
|
||||
const Library = lazy(() => import("./routes/library"));
|
||||
const Search = lazy(() => import("./routes/search"));
|
||||
const Artist = lazy(() => import("./routes/artists/[id]"));
|
||||
const Playlist = lazy(() => import("./routes/playlists/[id]"));
|
||||
const User = lazy(() => import("./routes/users/[id]"));
|
||||
const NotFound = lazy(() => import("./routes/not-found"));
|
||||
|
||||
render(<App />, document.getElementById("root")!);
|
||||
const root = document.getElementById("root");
|
||||
|
||||
// Hide splashscreen when loaded.
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
invoke("close_splashscreen"),
|
||||
render(
|
||||
() => (
|
||||
<Router root={App}>
|
||||
<Route path="/" component={Library} />
|
||||
<Route path="/search" component={Search} />
|
||||
|
||||
<Route path="/artists/:artistId" component={Artist} />
|
||||
|
||||
<Route path="/playlists/:playlistId" component={Playlist} />
|
||||
|
||||
<Route path="/users/:userId" component={User} />
|
||||
|
||||
<Route path="*404" component={NotFound} />
|
||||
</Router>
|
||||
),
|
||||
root!,
|
||||
);
|
||||
|
|
16
src/routes/artists/[id].tsx
Normal file
16
src/routes/artists/[id].tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { useParams } from "@solidjs/router";
|
||||
import { createResource } from "solid-js";
|
||||
|
||||
import { fetchAPI } from "@/lib/api";
|
||||
import { Artist as ArtistType } from "@/types/artist";
|
||||
|
||||
export default function Artist() {
|
||||
const params = useParams<{ artistId: string }>();
|
||||
const [artist] = createResource(() => params.artistId, getArtist);
|
||||
|
||||
return <p>{artist()?.name}</p>;
|
||||
}
|
||||
|
||||
async function getArtist(artistId: string): Promise<ArtistType> {
|
||||
return fetchAPI(`/artists/${artistId}`);
|
||||
}
|
7
src/routes/library.tsx
Normal file
7
src/routes/library.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function Library() {
|
||||
return (
|
||||
<div>
|
||||
<h1 class="headline">Library</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
15
src/routes/not-found.tsx
Normal file
15
src/routes/not-found.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
export default function NotFound() {
|
||||
return (
|
||||
<div class="flex size-full flex-col items-center justify-center gap-8">
|
||||
<h1 class="text-3xl font-bold">Page not found</h1>
|
||||
<div>
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-md border bg-primary px-4 py-2 text-secondary transition-colors hover:text-primary"
|
||||
>
|
||||
Back to Library
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
59
src/routes/playlists/[id].tsx
Normal file
59
src/routes/playlists/[id].tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { A, useParams } from "@solidjs/router";
|
||||
import { For, Show, createResource } from "solid-js";
|
||||
|
||||
import { getPlaylist, parseDuration } from "@/lib/playlists";
|
||||
import NotFound from "../not-found";
|
||||
import Track from "./track";
|
||||
|
||||
export default function Playlist() {
|
||||
const params = useParams<{ playlistId: string }>();
|
||||
const [playlist] = createResource(() => params.playlistId, getPlaylist);
|
||||
|
||||
return (
|
||||
<Show when={playlist()} fallback={<NotFound />}>
|
||||
<section class="flex flex-col gap-12">
|
||||
<header class="flex items-center gap-8">
|
||||
<div class="size-56 rounded-xl border bg-tertiary" />
|
||||
<div class="flex flex-col gap-4 text-secondary">
|
||||
<h1 class="text-3xl font-bold text-primary">{playlist()?.name}</h1>
|
||||
<p>
|
||||
Created by{" "}
|
||||
<A
|
||||
href={`/users/${playlist()?.creator.id}`}
|
||||
class="text-primary hover:underline"
|
||||
>
|
||||
{playlist()?.creator.name}
|
||||
</A>
|
||||
</p>
|
||||
<p class="text-xs uppercase tracking-widest">
|
||||
{playlist()?.tracks_count} Track
|
||||
{playlist()?.tracks_count === 1 ? "" : "s"} –{" "}
|
||||
{parseDuration(playlist()?.duration ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div aria-colcount={5}>
|
||||
<div class="playlist-row sticky -top-4 mb-2 border-b bg-secondary text-sm uppercase text-secondary">
|
||||
<span></span>
|
||||
<span>Title</span>
|
||||
<span>Album</span>
|
||||
<span class="hidden text-center md:block">Date added</span>
|
||||
<span class="text-center">Duration</span>
|
||||
</div>
|
||||
<For each={playlist()?.tracks} fallback={<EmptyPlaylist />}>
|
||||
{(track) => <Track track={track} />}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyPlaylist() {
|
||||
return (
|
||||
<div class="flex h-12 items-center justify-center text-center">
|
||||
😭 No tracks...
|
||||
</div>
|
||||
);
|
||||
}
|
62
src/routes/playlists/track.tsx
Normal file
62
src/routes/playlists/track.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { A } from "@solidjs/router";
|
||||
import { For, createSignal } from "solid-js";
|
||||
|
||||
import * as Icons from "@/icons";
|
||||
|
||||
import { Track as TrackType } from "@/types/track";
|
||||
import time, { parseDuration } from "@/lib/time";
|
||||
import { cn } from "@/lib/utils";
|
||||
import player from "@/lib/player";
|
||||
|
||||
interface TrackProps {
|
||||
track: TrackType;
|
||||
}
|
||||
|
||||
export default function Track({ track }: TrackProps) {
|
||||
const [liked, setLiked] = createSignal(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="playlist-row mb-2 cursor-pointer text-secondary transition-colors hover:bg-tertiary"
|
||||
onDblClick={() => player.changeTrack(track)}
|
||||
>
|
||||
<div class="size-12 rounded-md border bg-tertiary" />
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base text-primary">{track.title}</span>
|
||||
<span class="text-sm">
|
||||
<For each={track.artists}>
|
||||
{(artist, i) => (
|
||||
<>
|
||||
<A href={`/artists/${artist.id}`} class="hover:underline">
|
||||
{artist.name}
|
||||
</A>
|
||||
{i() < track.artists.length - 1 && ", "}
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm">None</span>
|
||||
<span class="hidden text-center text-sm md:block">
|
||||
{Intl.DateTimeFormat("en-US", { dateStyle: "medium" }).format(
|
||||
new Date(track.added_at),
|
||||
)}
|
||||
</span>
|
||||
<span class="text-center text-sm">
|
||||
{time.duration(parseDuration(track.duration_ms)).format("m:ss")}
|
||||
</span>
|
||||
<div>
|
||||
<button onClick={() => setLiked(!liked())} class="hover:text-accent">
|
||||
<Icons.FavouriteStroke
|
||||
class={cn(
|
||||
"size-6 transition-colors hover:scale-110 hover:fill-accent",
|
||||
{
|
||||
"fill-accent": liked(),
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
3
src/routes/search.tsx
Normal file
3
src/routes/search.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Search() {
|
||||
return <h1>SEarch</h1>;
|
||||
}
|
7
src/routes/settings.tsx
Normal file
7
src/routes/settings.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default function Settings() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
154
src/routes/users/[id].tsx
Normal file
154
src/routes/users/[id].tsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import { A, useParams } from "@solidjs/router";
|
||||
import { For, Show, createResource, createSignal, useContext } from "solid-js";
|
||||
|
||||
import { fetchAPI } from "@/lib/api";
|
||||
import { Playlist } from "@/types/playlist";
|
||||
import NotFound from "../not-found";
|
||||
import { UserContext } from "@/contexts/user";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function User() {
|
||||
const { user: loggedInUser } = useContext(UserContext);
|
||||
const params = useParams<{ userId: string }>();
|
||||
|
||||
const [user] = createResource(() => params.userId, getUser);
|
||||
const [playlists] = createResource(() => params.userId, getUserPlaylists);
|
||||
|
||||
const isSelf = loggedInUser.id === user()?.id;
|
||||
|
||||
const [followed, setFollowed] = createSignal(false);
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={!user.loading}
|
||||
fallback={
|
||||
<Show
|
||||
when={user.loading}
|
||||
fallback={<NotFound />}
|
||||
children={<p>Loading...</p>}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<section>
|
||||
<header class="mb-8 flex items-center gap-4 border-b pb-8">
|
||||
<div class="aspect-square size-40 rounded-full bg-tertiary xl:size-48" />
|
||||
<div class="flex flex-col items-start gap-4">
|
||||
<h1 class="text-3xl font-bold">{user()?.name}</h1>
|
||||
<p class="text-sm uppercase tracking-widest text-secondary">
|
||||
x Followers - x Following
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class={cn(
|
||||
"ml-auto rounded-md border bg-primary px-4 py-2 text-secondary transition-colors hover:bg-secondary hover:text-primary",
|
||||
{
|
||||
"bg-white text-secondary": followed(),
|
||||
},
|
||||
)}
|
||||
onClick={() => setFollowed(!followed())}
|
||||
>
|
||||
{followed() ? "Unfollow" : "Follow"}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-8 md:gap-16">
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl font-bold">Public Playlists</h2>
|
||||
<div class="grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
<For each={playlists()}>
|
||||
{(playlist) => (
|
||||
<A
|
||||
href={`/playlists/${playlist.id}`}
|
||||
class="flex flex-col overflow-hidden rounded-lg border bg-primary transition-transform hover:scale-105"
|
||||
>
|
||||
<div class="aspect-square w-full bg-tertiary" />
|
||||
|
||||
<div class="p-2">
|
||||
<h4 class="text-lg font-bold">{playlist.name}</h4>
|
||||
<p class="line-clamp-2 text-sm leading-tight text-secondary">
|
||||
Lorem ipsum dolor sit amet consectetur consectetur
|
||||
consectetur
|
||||
</p>
|
||||
</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl font-bold">Followers</h2>
|
||||
<div class="grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
<For each={playlists()}>
|
||||
{(playlist) => (
|
||||
<A
|
||||
href={`/playlists/${playlist.id}`}
|
||||
class="flex flex-col overflow-hidden rounded-lg border bg-primary transition-transform hover:scale-105"
|
||||
>
|
||||
<div class="aspect-square w-full bg-tertiary" />
|
||||
|
||||
<div class="p-2">
|
||||
<h4 class="text-lg font-bold">{playlist.name}</h4>
|
||||
<p class="line-clamp-2 text-sm leading-tight text-secondary">
|
||||
Lorem ipsum dolor sit amet consectetur consectetur
|
||||
consectetur
|
||||
</p>
|
||||
</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl font-bold">Following</h2>
|
||||
<div class="grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
<For each={playlists()}>
|
||||
{(playlist) => (
|
||||
<A
|
||||
href={`/playlists/${playlist.id}`}
|
||||
class="flex flex-col overflow-hidden rounded-lg border bg-primary transition-transform hover:scale-105"
|
||||
>
|
||||
<div class="aspect-square w-full bg-tertiary" />
|
||||
|
||||
<div class="p-2">
|
||||
<h4 class="text-lg font-bold">{playlist.name}</h4>
|
||||
<p class="line-clamp-2 text-sm leading-tight text-secondary">
|
||||
Lorem ipsum dolor sit amet consectetur consectetur
|
||||
consectetur
|
||||
</p>
|
||||
</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
const users = new Map<string, UserType>();
|
||||
|
||||
async function getUser(userId: string): Promise<UserType> {
|
||||
if (users.has(userId)) {
|
||||
return users.get(userId)!;
|
||||
}
|
||||
|
||||
const user = await fetchAPI<UserType>(`/users/${userId}`);
|
||||
|
||||
users.set(userId, user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getUserPlaylists(userId: string): Promise<Playlist[]> {
|
||||
return fetchAPI(`/users/${userId}/playlists`);
|
||||
}
|
||||
|
||||
interface UserType {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { signal } from "@preact/signals";
|
||||
|
||||
export const dragging = signal({
|
||||
isDragging: false,
|
||||
width: Number(localStorage.getItem("sidebar-width") ?? 256),
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
font-weight: 400;
|
||||
src: url("./assets/fonts/Rubik-Regular.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
font-weight: 700;
|
||||
src: url("./assets/fonts/Rubik-Bold.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
font-weight: 900;
|
||||
src: url("./assets/fonts/Rubik-Black.woff2");
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Rubik";
|
||||
background-color: #131317;
|
||||
color: #eeeeff;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply select-none bg-primary text-primary;
|
||||
}
|
4
src/types/artist.ts
Normal file
4
src/types/artist.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Artist {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
13
src/types/playlist.ts
Normal file
13
src/types/playlist.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Artist } from "./artist";
|
||||
import { Track } from "./track";
|
||||
|
||||
export interface Playlist {
|
||||
id: string;
|
||||
name: string;
|
||||
playlist_type: "playlist" | "folder";
|
||||
parent_id: string | null;
|
||||
creator: Artist;
|
||||
tracks: Track[];
|
||||
tracks_count: number;
|
||||
duration: number;
|
||||
}
|
19
src/types/track.ts
Normal file
19
src/types/track.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Artist } from "./artist";
|
||||
|
||||
export interface Track {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string;
|
||||
duration: number;
|
||||
artists: Artist[];
|
||||
|
||||
added_at?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_TRACK: Track = {
|
||||
id: "",
|
||||
title: "",
|
||||
image: "",
|
||||
duration: 0,
|
||||
artists: [],
|
||||
};
|
|
@ -1,74 +1,6 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config = {
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{ts,tsx}"],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
textColor: {},
|
||||
|
||||
backgroundColor: {
|
||||
primary: "#131317",
|
||||
secondary: "#16161C",
|
||||
tertiary: "#23202C",
|
||||
},
|
||||
|
||||
colors: {
|
||||
accent: "#959EEE",
|
||||
border: "#232332",
|
||||
|
||||
primary: "#EEEEFF",
|
||||
secondary: "#7A7A9A",
|
||||
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
||||
content: ["./src/**/*.{html,js,jsx,md,mdx,ts,tsx}"],
|
||||
presets: [require("./ui.preset.js")],
|
||||
};
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
/* Linting */
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": ["./src-tauri"],
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
|
|
11
ui.config.json
Normal file
11
ui.config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"tsx": true,
|
||||
"componentDir": "./src/components/ui",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/index.css"
|
||||
},
|
||||
"aliases": {
|
||||
"path": "@/*"
|
||||
}
|
||||
}
|
103
ui.preset.js
Normal file
103
ui.preset.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
const { fontFamily } = require("tailwindcss/defaultTheme");
|
||||
|
||||
/**@type {import("tailwindcss").Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{js,jsx,md,mdx,ts,tsx}"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
textColor: {},
|
||||
|
||||
backgroundColor: {
|
||||
primary: "#131317",
|
||||
secondary: "#16161C",
|
||||
tertiary: "#23202C",
|
||||
},
|
||||
|
||||
colors: {
|
||||
accent: "#959EEE",
|
||||
border: "#232332",
|
||||
|
||||
primary: "#EEEEFF",
|
||||
secondary: "#7A7A9A",
|
||||
|
||||
// destructive: {
|
||||
// DEFAULT: "hsl(var(--destructive))",
|
||||
// foreground: "hsl(var(--destructive-foreground))",
|
||||
// },
|
||||
// info: {
|
||||
// DEFAULT: "hsl(var(--info))",
|
||||
// foreground: "hsl(var(--info-foreground))",
|
||||
// },
|
||||
// success: {
|
||||
// DEFAULT: "hsl(var(--success))",
|
||||
// foreground: "hsl(var(--success-foreground))",
|
||||
// },
|
||||
// warning: {
|
||||
// DEFAULT: "hsl(var(--warning))",
|
||||
// foreground: "hsl(var(--warning-foreground))",
|
||||
// },
|
||||
// error: {
|
||||
// DEFAULT: "hsl(var(--error))",
|
||||
// foreground: "hsl(var(--error-foreground))",
|
||||
// },
|
||||
// muted: {
|
||||
// DEFAULT: "hsl(var(--muted))",
|
||||
// foreground: "hsl(var(--muted-foreground))",
|
||||
// },
|
||||
// accent: {
|
||||
// DEFAULT: "hsl(var(--accent))",
|
||||
// foreground: "hsl(var(--accent-foreground))",
|
||||
// },
|
||||
// popover: {
|
||||
// DEFAULT: "hsl(var(--popover))",
|
||||
// foreground: "hsl(var(--popover-foreground))",
|
||||
// },
|
||||
// card: {
|
||||
// DEFAULT: "hsl(var(--card))",
|
||||
// foreground: "hsl(var(--card-foreground))",
|
||||
// },
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Inter", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--kb-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--kb-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"content-show": {
|
||||
from: { opacity: 0, transform: "scale(0.96)" },
|
||||
to: { opacity: 1, transform: "scale(1)" },
|
||||
},
|
||||
"content-hide": {
|
||||
from: { opacity: 1, transform: "scale(1)" },
|
||||
to: { opacity: 0, transform: "scale(0.96)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"content-show": "content-show 0.2s ease-out",
|
||||
"content-hide": "content-hide 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
|
@ -1,17 +1,15 @@
|
|||
import path from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import preact from "@preact/preset-vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [preact()],
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
preserveSymlinks: true,
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
|
@ -27,4 +25,4 @@ export default defineConfig(async () => ({
|
|||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue