This commit is contained in:
Miguel da Mota 2024-02-11 18:12:40 +01:00
parent 5c0034aabd
commit 9ad8d255de
70 changed files with 3498 additions and 2727 deletions

View file

@ -1 +1,3 @@
src-tauri
src/components/ui
dist

View file

@ -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
View file

@ -0,0 +1 @@
ignore-workspace-root-check=true

View file

@ -1 +1,2 @@
src-tauri
src/components/ui

View file

@ -1,3 +1,4 @@
module.exports = {
plugins: ["prettier-plugin-tailwindcss"],
useTabs: true,
};

View file

@ -1,3 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

31
.zed/scripts/eslint.cjs Normal file
View 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
View 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
}
}
}

View file

@ -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"
}
}

View file

@ -1,14 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<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>
</head>
<head>
<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>Vybr: Music Player</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,40 +1,45 @@
{
"name": "tauri-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@preact/signals": "^1.2.2",
"@preact/signals-core": "^1.5.1",
"@tauri-apps/api": "^1.5.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.309.0",
"preact": "^10.16.0",
"tailwind-merge": "^2.2.0"
},
"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",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"postcss": "^8.4.33",
"prettier": "^3.2.1",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.3",
"vite": "^5.0.0"
}
"name": "tauri-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@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",
"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": {
"@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",
"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.1.1",
"vite-plugin-solid": "^2.9.1"
}
}

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
packages:
- "packages/*"

View file

@ -1,22 +1,22 @@
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #131317;
color: #fff;
body {
background-color: #131317;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
width: 100dvw;
height: 100dvh;
}
display: flex;
align-items: center;
justify-content: center;
width: 100dvw;
height: 100dvh;
}
</style>
<div>
<h3>Loading...</h3>
<h3>Loading...</h3>
</div>

View file

@ -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

View file

@ -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
View file

@ -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",

View file

@ -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"

View file

@ -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();

View file

@ -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
}
]
}

View file

@ -15,7 +15,9 @@
"height": 400,
"decorations": false,
"url": "splashscreen.html",
"label": "splashscreen"
"label": "splashscreen",
"resizable": false,
"fullscreen": false
}
]
}

View file

@ -1,7 +0,0 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.preact:hover {
filter: drop-shadow(0 0 2em #673ab8);
}

View file

@ -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 (
<div
className={cn("select-none", {
"cursor-col-resize": dragging.value.isDragging,
})}
>
{isWindows() && <TitleBar />}
<UserProvider>
<div class="grid h-[100dvh] grid-rows-[1fr,auto]">
<div
class={cn(
"grid h-full min-h-full grid-cols-[auto,1fr,auto] gap-2 bg-primary/90 p-2 pb-0 backdrop-blur",
)}
>
<Sidebar />
<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>
</div>
</UserProvider>
);
}

View file

@ -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
View 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>
);
}

View file

@ -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>
);
}

View 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>
);
}

View 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>
);
}

View file

@ -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>
);
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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;

View 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
View 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
View 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);
}

View 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: "",
};

View 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";

View file

@ -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)!;
}
const playlist = await fetchAPI<Playlist>(`/playlists/${playlistId}`);
playlists.set(playlistId, playlist);
return playlist;
}
export function getPlaylists(): Playlist[] {
return [];
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
View 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
View 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 };
}

View file

@ -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))
}

View file

@ -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!,
);

View 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
View 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
View 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>
);
}

View 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>
);
}

View 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
View file

@ -0,0 +1,3 @@
export default function Search() {
return <h1>SEarch</h1>;
}

7
src/routes/settings.tsx Normal file
View file

@ -0,0 +1,7 @@
export default function Settings() {
return (
<div>
<h1>Settings</h1>
</div>
);
}

154
src/routes/users/[id].tsx Normal file
View 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;
}

View file

@ -1,6 +0,0 @@
import { signal } from "@preact/signals";
export const dragging = signal({
isDragging: false,
width: Number(localStorage.getItem("sidebar-width") ?? 256),
});

View file

@ -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
View file

@ -0,0 +1,4 @@
export interface Artist {
id: string;
name: string;
}

13
src/types/playlist.ts Normal file
View 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
View 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: [],
};

View file

@ -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")],
};

View file

@ -1,33 +1,38 @@
{
"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
View 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
View 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")],
};

View file

@ -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/**"],
},
},
}));
});