v7.0
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300..800&family=Figtree:wght@300..900&family=Geist:wght@100..900&family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Google+Sans+Flex:opsz,wght@6..144,1..1000&family=Inter:wght@300..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:wght@300..800&family=Noto+Sans:wght@100..900&family=Nunito+Sans:opsz,wght@6..12,200..1000&family=Outfit:wght@100..900&family=Plus+Jakarta+Sans:wght@300..800&family=Poppins:wght@300;400;500;600;700;800&family=Public+Sans:ital,wght@0,100..900;1,100..900&family=Raleway:wght@100..900&family=Roboto:wght@300;400;500;700;900&family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300..800&family=Figtree:wght@300..900&family=Geist:wght@100..900&family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&family=Inter:wght@300..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Manrope:wght@300..800&family=Noto+Sans:wght@100..900&family=Nunito+Sans:opsz,wght@6..12,200..1000&family=Outfit:wght@100..900&family=Plus+Jakarta+Sans:wght@300..800&family=Poppins:wght@300;400;500;600;700;800&family=Public+Sans:ital,wght@0,100..900;1,100..900&family=Raleway:wght@100..900&family=Roboto:wght@300;400;500;700;900&family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
|
||||
<title>SpotiFLAC</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.12.1",
|
||||
"motion": "^12.23.26",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
@@ -48,7 +48,7 @@
|
||||
"sharp": "^0.34.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
c94dda3302d3338d7909ef5d634d0fde
|
||||
0f9764c2a4597a75120d3e76c32af7a9
|
||||
Generated
+165
-165
@@ -54,7 +54,7 @@ importers:
|
||||
specifier: ^0.562.0
|
||||
version: 0.562.0(react@19.2.3)
|
||||
motion:
|
||||
specifier: ^12.12.1
|
||||
specifier: ^12.23.26
|
||||
version: 12.23.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
@@ -112,8 +112,8 @@ importers:
|
||||
specifier: ~5.9.3
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.50.0
|
||||
version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
specifier: ^8.50.1
|
||||
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
vite:
|
||||
specifier: ^7.3.0
|
||||
version: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||
@@ -1026,113 +1026,113 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.53.5':
|
||||
resolution: {integrity: sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==}
|
||||
'@rollup/rollup-android-arm-eabi@4.54.0':
|
||||
resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==}
|
||||
'@rollup/rollup-android-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==}
|
||||
'@rollup/rollup-darwin-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.53.5':
|
||||
resolution: {integrity: sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==}
|
||||
'@rollup/rollup-darwin-x64@4.54.0':
|
||||
resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==}
|
||||
'@rollup/rollup-freebsd-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.53.5':
|
||||
resolution: {integrity: sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==}
|
||||
'@rollup/rollup-freebsd-x64@4.54.0':
|
||||
resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.53.5':
|
||||
resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==}
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
|
||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.5':
|
||||
resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==}
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==}
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==}
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==}
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==}
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==}
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==}
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==}
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==}
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==}
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==}
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.53.5':
|
||||
resolution: {integrity: sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==}
|
||||
'@rollup/rollup-win32-arm64-msvc@4.54.0':
|
||||
resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.53.5':
|
||||
resolution: {integrity: sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==}
|
||||
'@rollup/rollup-win32-ia32-msvc@4.54.0':
|
||||
resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==}
|
||||
'@rollup/rollup-win32-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.53.5':
|
||||
resolution: {integrity: sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==}
|
||||
'@rollup/rollup-win32-x64-msvc@4.54.0':
|
||||
resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -1255,63 +1255,63 @@ packages:
|
||||
'@types/react@19.2.7':
|
||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.50.0':
|
||||
resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==}
|
||||
'@typescript-eslint/eslint-plugin@8.50.1':
|
||||
resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.50.0
|
||||
'@typescript-eslint/parser': ^8.50.1
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/parser@8.50.0':
|
||||
resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==}
|
||||
'@typescript-eslint/parser@8.50.1':
|
||||
resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.50.0':
|
||||
resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==}
|
||||
'@typescript-eslint/project-service@8.50.1':
|
||||
resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@8.50.0':
|
||||
resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==}
|
||||
'@typescript-eslint/scope-manager@8.50.1':
|
||||
resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.50.0':
|
||||
resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==}
|
||||
'@typescript-eslint/tsconfig-utils@8.50.1':
|
||||
resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.50.0':
|
||||
resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==}
|
||||
'@typescript-eslint/type-utils@8.50.1':
|
||||
resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/types@8.50.0':
|
||||
resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==}
|
||||
'@typescript-eslint/types@8.50.1':
|
||||
resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.50.0':
|
||||
resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==}
|
||||
'@typescript-eslint/typescript-estree@8.50.1':
|
||||
resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/utils@8.50.0':
|
||||
resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==}
|
||||
'@typescript-eslint/utils@8.50.1':
|
||||
resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.50.0':
|
||||
resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==}
|
||||
'@typescript-eslint/visitor-keys@8.50.1':
|
||||
resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vitejs/plugin-react@5.1.2':
|
||||
@@ -1347,8 +1347,8 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
baseline-browser-mapping@2.9.10:
|
||||
resolution: {integrity: sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==}
|
||||
baseline-browser-mapping@2.9.11:
|
||||
resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==}
|
||||
hasBin: true
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
@@ -1366,8 +1366,8 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001760:
|
||||
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
|
||||
caniuse-lite@1.0.30001761:
|
||||
resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
@@ -1866,8 +1866,8 @@ packages:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
rollup@4.53.5:
|
||||
resolution: {integrity: sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==}
|
||||
rollup@4.54.0:
|
||||
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1943,8 +1943,8 @@ packages:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
typescript-eslint@8.50.0:
|
||||
resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==}
|
||||
typescript-eslint@8.50.1:
|
||||
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
@@ -2865,70 +2865,70 @@ snapshots:
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.53.5':
|
||||
'@rollup/rollup-android-arm-eabi@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.53.5':
|
||||
'@rollup/rollup-android-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.53.5':
|
||||
'@rollup/rollup-darwin-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.53.5':
|
||||
'@rollup/rollup-darwin-x64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.53.5':
|
||||
'@rollup/rollup-freebsd-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.53.5':
|
||||
'@rollup/rollup-freebsd-x64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.53.5':
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.5':
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.5':
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.5':
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.5':
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.5':
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.5':
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.5':
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.5':
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.5':
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.5':
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.5':
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.53.5':
|
||||
'@rollup/rollup-win32-arm64-msvc@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.53.5':
|
||||
'@rollup/rollup-win32-ia32-msvc@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.53.5':
|
||||
'@rollup/rollup-win32-x64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.53.5':
|
||||
'@rollup/rollup-win32-x64-msvc@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/node@4.1.18':
|
||||
@@ -3036,14 +3036,14 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.50.0
|
||||
'@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
'@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.50.1
|
||||
'@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.50.1
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
@@ -3052,41 +3052,41 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.50.0
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
'@typescript-eslint/scope-manager': 8.50.1
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.50.1
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.50.0(typescript@5.9.3)':
|
||||
'@typescript-eslint/project-service@8.50.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
debug: 4.4.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.50.0':
|
||||
'@typescript-eslint/scope-manager@8.50.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
'@typescript-eslint/visitor-keys': 8.50.1
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)':
|
||||
'@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
@@ -3094,14 +3094,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.50.0': {}
|
||||
'@typescript-eslint/types@8.50.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)':
|
||||
'@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/visitor-keys': 8.50.0
|
||||
'@typescript-eslint/project-service': 8.50.1(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
'@typescript-eslint/visitor-keys': 8.50.1
|
||||
debug: 4.4.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
@@ -3111,20 +3111,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
|
||||
'@typescript-eslint/scope-manager': 8.50.0
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.50.1
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.50.0':
|
||||
'@typescript-eslint/visitor-keys@8.50.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.50.0
|
||||
'@typescript-eslint/types': 8.50.1
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@vitejs/plugin-react@5.1.2(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2))':
|
||||
@@ -3164,7 +3164,7 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
baseline-browser-mapping@2.9.10: {}
|
||||
baseline-browser-mapping@2.9.11: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
dependencies:
|
||||
@@ -3177,15 +3177,15 @@ snapshots:
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.9.10
|
||||
caniuse-lite: 1.0.30001760
|
||||
baseline-browser-mapping: 2.9.11
|
||||
caniuse-lite: 1.0.30001761
|
||||
electron-to-chromium: 1.5.267
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001760: {}
|
||||
caniuse-lite@1.0.30001761: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
@@ -3634,32 +3634,32 @@ snapshots:
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
rollup@4.53.5:
|
||||
rollup@4.54.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.53.5
|
||||
'@rollup/rollup-android-arm64': 4.53.5
|
||||
'@rollup/rollup-darwin-arm64': 4.53.5
|
||||
'@rollup/rollup-darwin-x64': 4.53.5
|
||||
'@rollup/rollup-freebsd-arm64': 4.53.5
|
||||
'@rollup/rollup-freebsd-x64': 4.53.5
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.53.5
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.53.5
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.53.5
|
||||
'@rollup/rollup-linux-arm64-musl': 4.53.5
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.53.5
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.53.5
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.53.5
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.53.5
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.53.5
|
||||
'@rollup/rollup-linux-x64-gnu': 4.53.5
|
||||
'@rollup/rollup-linux-x64-musl': 4.53.5
|
||||
'@rollup/rollup-openharmony-arm64': 4.53.5
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.53.5
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.53.5
|
||||
'@rollup/rollup-win32-x64-gnu': 4.53.5
|
||||
'@rollup/rollup-win32-x64-msvc': 4.53.5
|
||||
'@rollup/rollup-android-arm-eabi': 4.54.0
|
||||
'@rollup/rollup-android-arm64': 4.54.0
|
||||
'@rollup/rollup-darwin-arm64': 4.54.0
|
||||
'@rollup/rollup-darwin-x64': 4.54.0
|
||||
'@rollup/rollup-freebsd-arm64': 4.54.0
|
||||
'@rollup/rollup-freebsd-x64': 4.54.0
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.54.0
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.54.0
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-arm64-musl': 4.54.0
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.54.0
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-x64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-x64-musl': 4.54.0
|
||||
'@rollup/rollup-openharmony-arm64': 4.54.0
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.54.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.54.0
|
||||
'@rollup/rollup-win32-x64-gnu': 4.54.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.54.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
@@ -3741,12 +3741,12 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
typescript-eslint@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -3787,7 +3787,7 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.53.5
|
||||
rollup: 4.54.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
+72
-23
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { Search, X, ArrowUp } from "lucide-react";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { getSettings, applyThemeMode, applyFont } from "@/lib/settings";
|
||||
import { getSettings, getSettingsWithDefaults, saveSettings, applyThemeMode, applyFont } from "@/lib/settings";
|
||||
import { applyTheme } from "@/lib/themes";
|
||||
import { OpenFolder } from "../wailsjs/go/main/App";
|
||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||
@@ -55,9 +55,11 @@ function App() {
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [releaseDate, setReleaseDate] = useState<string | null>(null);
|
||||
const [fetchHistory, setFetchHistory] = useState<HistoryItem[]>([]);
|
||||
const [isSearchMode, setIsSearchMode] = useState(false);
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
|
||||
const ITEMS_PER_PAGE = 50;
|
||||
const CURRENT_VERSION = "6.9";
|
||||
const CURRENT_VERSION = "7.0";
|
||||
|
||||
const download = useDownload();
|
||||
const metadata = useMetadata();
|
||||
@@ -68,10 +70,19 @@ function App() {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const settings = getSettings();
|
||||
applyThemeMode(settings.themeMode);
|
||||
applyTheme(settings.theme);
|
||||
applyFont(settings.fontFamily);
|
||||
const initSettings = async () => {
|
||||
const settings = getSettings();
|
||||
applyThemeMode(settings.themeMode);
|
||||
applyTheme(settings.theme);
|
||||
applyFont(settings.fontFamily);
|
||||
|
||||
// Initialize default download path if not set
|
||||
if (!settings.downloadPath) {
|
||||
const settingsWithDefaults = await getSettingsWithDefaults();
|
||||
saveSettings(settingsWithDefaults);
|
||||
}
|
||||
};
|
||||
initSettings();
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = () => {
|
||||
@@ -86,11 +97,22 @@ function App() {
|
||||
checkForUpdates();
|
||||
loadHistory();
|
||||
|
||||
// Scroll listener for jump to top button
|
||||
const handleScroll = () => {
|
||||
setShowScrollTop(window.scrollY > 300);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scrollToTop = useCallback(() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTracks([]);
|
||||
setSearchQuery("");
|
||||
@@ -282,10 +304,17 @@ function App() {
|
||||
checkingAvailability={availability.checkingTrackId === track.spotify_id}
|
||||
availability={availability.getAvailability(track.spotify_id || "")}
|
||||
downloadingCover={cover.downloadingCover}
|
||||
downloadedCover={cover.downloadedCovers.has(track.spotify_id || "")}
|
||||
failedCover={cover.failedCovers.has(track.spotify_id || "")}
|
||||
skippedCover={cover.skippedCovers.has(track.spotify_id || "")}
|
||||
onDownload={download.handleDownloadTrack}
|
||||
onDownloadLyrics={lyrics.handleDownloadLyrics}
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, undefined, undefined, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onCheckAvailability={availability.checkAvailability}
|
||||
onDownloadCover={cover.handleDownloadCover}
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, undefined, undefined, trackId, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onOpenFolder={handleOpenFolder}
|
||||
/>
|
||||
);
|
||||
@@ -327,11 +356,11 @@ function App() {
|
||||
onToggleTrack={toggleTrackSelection}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
onDownloadTrack={download.handleDownloadTrack}
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, position)
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, position, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, position, trackId)
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, position, trackId, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onCheckAvailability={availability.checkAvailability}
|
||||
onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, album_info.name)}
|
||||
@@ -395,11 +424,11 @@ function App() {
|
||||
onToggleTrack={toggleTrackSelection}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
onDownloadTrack={download.handleDownloadTrack}
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, position)
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, position, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, position, trackId)
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, position, trackId, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onCheckAvailability={availability.checkAvailability}
|
||||
onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlist_info.owner.name)}
|
||||
@@ -469,11 +498,11 @@ function App() {
|
||||
onToggleTrack={toggleTrackSelection}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
onDownloadTrack={download.handleDownloadTrack}
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, position)
|
||||
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) =>
|
||||
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, position, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, position, trackId)
|
||||
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) =>
|
||||
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, position, trackId, albumArtist, releaseDate, discNumber)
|
||||
}
|
||||
onCheckAvailability={availability.checkAvailability}
|
||||
onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, artist_info.name)}
|
||||
@@ -629,13 +658,22 @@ function App() {
|
||||
loading={metadata.loading}
|
||||
onUrlChange={setSpotifyUrl}
|
||||
onFetch={handleFetchMetadata}
|
||||
onFetchUrl={async (url) => {
|
||||
setSpotifyUrl(url);
|
||||
const updatedUrl = await metadata.handleFetchMetadata(url);
|
||||
if (updatedUrl) {
|
||||
setSpotifyUrl(updatedUrl);
|
||||
}
|
||||
}}
|
||||
history={fetchHistory}
|
||||
onHistorySelect={handleHistorySelect}
|
||||
onHistoryRemove={removeFromHistory}
|
||||
hasResult={!!metadata.metadata}
|
||||
searchMode={isSearchMode}
|
||||
onSearchModeChange={setIsSearchMode}
|
||||
/>
|
||||
|
||||
{metadata.metadata && renderMetadata()}
|
||||
{!isSearchMode && metadata.metadata && renderMetadata()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -662,6 +700,17 @@ function App() {
|
||||
isOpen={downloadQueue.isOpen}
|
||||
onClose={downloadQueue.closeQueue}
|
||||
/>
|
||||
|
||||
{/* Jump to Top Button - Bottom Right */}
|
||||
{showScrollTop && (
|
||||
<Button
|
||||
onClick={scrollToTop}
|
||||
className="fixed bottom-6 right-6 z-50 h-10 w-10 rounded-full shadow-lg"
|
||||
size="icon"
|
||||
>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -52,8 +52,8 @@ interface AlbumInfoProps {
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
onDownloadAllLyrics?: () => void;
|
||||
onDownloadAllCovers?: () => void;
|
||||
|
||||
@@ -57,8 +57,8 @@ interface ArtistInfoProps {
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
onDownloadAllLyrics?: () => void;
|
||||
onDownloadAllCovers?: () => void;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,8 +56,8 @@ interface PlaylistInfoProps {
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
onDownloadAllLyrics?: () => void;
|
||||
onDownloadAllCovers?: () => void;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InputWithContext } from "@/components/ui/input-with-context";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CloudDownload, Info, XCircle } from "lucide-react";
|
||||
import { CloudDownload, Info, XCircle, Link, Search, X, ChevronDown } from "lucide-react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -10,16 +10,28 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { FetchHistory } from "@/components/FetchHistory";
|
||||
import type { HistoryItem } from "@/components/FetchHistory";
|
||||
import { SearchSpotify, SearchSpotifyByType } from "../../wailsjs/go/main/App";
|
||||
import { backend } from "../../wailsjs/go/models";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ResultTab = "tracks" | "albums" | "artists" | "playlists";
|
||||
|
||||
const RECENT_SEARCHES_KEY = "spotiflac_recent_searches";
|
||||
const MAX_RECENT_SEARCHES = 8;
|
||||
const SEARCH_LIMIT = 50;
|
||||
|
||||
interface SearchBarProps {
|
||||
url: string;
|
||||
loading: boolean;
|
||||
onUrlChange: (url: string) => void;
|
||||
onFetch: () => void;
|
||||
onFetchUrl: (url: string) => Promise<void>;
|
||||
history: HistoryItem[];
|
||||
onHistorySelect: (item: HistoryItem) => void;
|
||||
onHistoryRemove: (id: string) => void;
|
||||
hasResult: boolean;
|
||||
searchMode: boolean;
|
||||
onSearchModeChange: (isSearch: boolean) => void;
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
@@ -27,68 +39,513 @@ export function SearchBar({
|
||||
loading,
|
||||
onUrlChange,
|
||||
onFetch,
|
||||
onFetchUrl,
|
||||
history,
|
||||
onHistorySelect,
|
||||
onHistoryRemove,
|
||||
hasResult,
|
||||
searchMode,
|
||||
onSearchModeChange,
|
||||
}: SearchBarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<backend.SearchResponse | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [lastSearchedQuery, setLastSearchedQuery] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<ResultTab>("tracks");
|
||||
const [recentSearches, setRecentSearches] = useState<string[]>([]);
|
||||
const [hasMore, setHasMore] = useState<Record<ResultTab, boolean>>({
|
||||
tracks: false,
|
||||
albums: false,
|
||||
artists: false,
|
||||
playlists: false,
|
||||
});
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Load recent searches from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(RECENT_SEARCHES_KEY);
|
||||
if (saved) {
|
||||
setRecentSearches(JSON.parse(saved));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load recent searches:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveRecentSearch = (query: string) => {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
setRecentSearches((prev) => {
|
||||
const filtered = prev.filter((s) => s.toLowerCase() !== trimmed.toLowerCase());
|
||||
const updated = [trimmed, ...filtered].slice(0, MAX_RECENT_SEARCHES);
|
||||
try {
|
||||
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated));
|
||||
} catch (error) {
|
||||
console.error("Failed to save recent searches:", error);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const removeRecentSearch = (query: string) => {
|
||||
setRecentSearches((prev) => {
|
||||
const updated = prev.filter((s) => s !== query);
|
||||
try {
|
||||
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated));
|
||||
} catch (error) {
|
||||
console.error("Failed to save recent searches:", error);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Debounced search - only search if query changed
|
||||
useEffect(() => {
|
||||
if (!searchMode || !searchQuery.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't search again if query is the same
|
||||
if (searchQuery.trim() === lastSearchedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
|
||||
searchTimeoutRef.current = setTimeout(async () => {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const results = await SearchSpotify({ query: searchQuery, limit: SEARCH_LIMIT });
|
||||
setSearchResults(results);
|
||||
setLastSearchedQuery(searchQuery.trim());
|
||||
saveRecentSearch(searchQuery.trim());
|
||||
|
||||
// Check if there might be more results
|
||||
setHasMore({
|
||||
tracks: results.tracks.length === SEARCH_LIMIT,
|
||||
albums: results.albums.length === SEARCH_LIMIT,
|
||||
artists: results.artists.length === SEARCH_LIMIT,
|
||||
playlists: results.playlists.length === SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
// Auto-select first tab with results
|
||||
if (results.tracks.length > 0) setActiveTab("tracks");
|
||||
else if (results.albums.length > 0) setActiveTab("albums");
|
||||
else if (results.artists.length > 0) setActiveTab("artists");
|
||||
else if (results.playlists.length > 0) setActiveTab("playlists");
|
||||
} catch (error) {
|
||||
console.error("Search failed:", error);
|
||||
setSearchResults(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, 400);
|
||||
|
||||
return () => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [searchQuery, searchMode, lastSearchedQuery]);
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (!searchResults || !lastSearchedQuery || isLoadingMore) return;
|
||||
|
||||
const typeMap: Record<ResultTab, string> = {
|
||||
tracks: "track",
|
||||
albums: "album",
|
||||
artists: "artist",
|
||||
playlists: "playlist",
|
||||
};
|
||||
|
||||
const currentCount = getTabCount(activeTab);
|
||||
|
||||
setIsLoadingMore(true);
|
||||
try {
|
||||
const moreResults = await SearchSpotifyByType({
|
||||
query: lastSearchedQuery,
|
||||
search_type: typeMap[activeTab],
|
||||
limit: SEARCH_LIMIT,
|
||||
offset: currentCount,
|
||||
});
|
||||
|
||||
if (moreResults.length > 0) {
|
||||
setSearchResults((prev) => {
|
||||
if (!prev) return prev;
|
||||
// Create new SearchResponse with updated array for the active tab
|
||||
const updated = new backend.SearchResponse({
|
||||
tracks: activeTab === "tracks" ? [...prev.tracks, ...moreResults] : prev.tracks,
|
||||
albums: activeTab === "albums" ? [...prev.albums, ...moreResults] : prev.albums,
|
||||
artists: activeTab === "artists" ? [...prev.artists, ...moreResults] : prev.artists,
|
||||
playlists: activeTab === "playlists" ? [...prev.playlists, ...moreResults] : prev.playlists,
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
// Update hasMore for this tab
|
||||
setHasMore((prev) => ({
|
||||
...prev,
|
||||
[activeTab]: moreResults.length === SEARCH_LIMIT,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Load more failed:", error);
|
||||
} finally {
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResultClick = (externalUrl: string) => {
|
||||
onSearchModeChange(false);
|
||||
onFetchUrl(externalUrl);
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const hasAnyResults = searchResults && (
|
||||
searchResults.tracks.length > 0 ||
|
||||
searchResults.albums.length > 0 ||
|
||||
searchResults.artists.length > 0 ||
|
||||
searchResults.playlists.length > 0
|
||||
);
|
||||
|
||||
const getTabCount = (tab: ResultTab): number => {
|
||||
if (!searchResults) return 0;
|
||||
switch (tab) {
|
||||
case "tracks": return searchResults.tracks.length;
|
||||
case "albums": return searchResults.albums.length;
|
||||
case "artists": return searchResults.artists.length;
|
||||
case "playlists": return searchResults.playlists.length;
|
||||
}
|
||||
};
|
||||
|
||||
const tabs: { key: ResultTab; label: string }[] = [
|
||||
{ key: "tracks", label: "Tracks" },
|
||||
{ key: "albums", label: "Albums" },
|
||||
{ key: "artists", label: "Artists" },
|
||||
{ key: "playlists", label: "Playlists" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="spotify-url">Spotify URL</Label>
|
||||
{/* Mode Toggle */}
|
||||
<div className="flex items-center bg-muted rounded-md p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSearchModeChange(false)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-2.5 py-1 rounded text-sm font-medium transition-colors cursor-pointer",
|
||||
!searchMode
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Link className="h-3.5 w-3.5" />
|
||||
URL
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSearchModeChange(true)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-2.5 py-1 rounded text-sm font-medium transition-colors cursor-pointer",
|
||||
searchMode
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Supports track, album, playlist, and artist URLs</p>
|
||||
<p className="mt-1">Note: Playlist must be public (not private)</p>
|
||||
{!searchMode ? (
|
||||
<>
|
||||
<p>Supports track, album, playlist, and artist URLs</p>
|
||||
<p className="mt-1">Note: Playlist must be public (not private)</p>
|
||||
</>
|
||||
) : (
|
||||
<p>Search for tracks, albums, artists, or playlists</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<InputWithContext
|
||||
id="spotify-url"
|
||||
placeholder="https://open.spotify.com/..."
|
||||
value={url}
|
||||
onChange={(e) => onUrlChange(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && onFetch()}
|
||||
className="pr-8"
|
||||
/>
|
||||
{url && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
onClick={() => onUrlChange("")}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={onFetch} disabled={loading}>
|
||||
{loading ? (
|
||||
{!searchMode ? (
|
||||
<>
|
||||
<Spinner />
|
||||
Fetching...
|
||||
<InputWithContext
|
||||
id="spotify-url"
|
||||
placeholder="https://open.spotify.com/..."
|
||||
value={url}
|
||||
onChange={(e) => onUrlChange(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && onFetch()}
|
||||
className="pr-8"
|
||||
/>
|
||||
{url && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
onClick={() => onUrlChange("")}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudDownload className="h-4 w-4" />
|
||||
Fetch
|
||||
<InputWithContext
|
||||
id="spotify-search"
|
||||
placeholder="Search tracks, albums, artists..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pr-8"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setSearchResults(null);
|
||||
setLastSearchedQuery("");
|
||||
}}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!searchMode && (
|
||||
<Button onClick={onFetch} disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
Fetching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudDownload className="h-4 w-4" />
|
||||
Fetch
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!hasResult && (
|
||||
|
||||
{!searchMode && !hasResult && (
|
||||
<FetchHistory
|
||||
history={history}
|
||||
onSelect={onHistorySelect}
|
||||
onRemove={onHistoryRemove}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Search Results with Tabs */}
|
||||
{searchMode && (
|
||||
<div className="space-y-4">
|
||||
{/* Recent Searches - show when no query or no results yet */}
|
||||
{!searchQuery && !searchResults && recentSearches.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">Recent Searches</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recentSearches.map((query) => (
|
||||
<div
|
||||
key={query}
|
||||
className="group relative flex items-center px-3 py-1.5 bg-muted hover:bg-accent rounded-full text-sm cursor-pointer transition-colors"
|
||||
onClick={() => setSearchQuery(query)}
|
||||
>
|
||||
<span>{query}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute -top-1.5 -right-1.5 z-10 w-5 h-5 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all cursor-pointer shadow-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeRecentSearch(query);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-900" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSearching && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner />
|
||||
<span className="ml-2 text-muted-foreground">Searching...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && searchQuery && !hasAnyResults && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No results found for "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && hasAnyResults && (
|
||||
<>
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 border-b">
|
||||
{tabs.map((tab) => {
|
||||
const count = getTabCount(tab.key);
|
||||
if (count === 0) return null;
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-medium transition-colors cursor-pointer border-b-2 -mb-px",
|
||||
activeTab === tab.key
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{tab.label} ({count})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="grid gap-2">
|
||||
{/* Tracks */}
|
||||
{activeTab === "tracks" && searchResults?.tracks.map((track) => (
|
||||
<button
|
||||
key={track.id}
|
||||
type="button"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors"
|
||||
onClick={() => handleResultClick(track.external_urls)}
|
||||
>
|
||||
{track.images ? (
|
||||
<img src={track.images} alt="" className="w-12 h-12 rounded object-cover shrink-0" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded bg-muted shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{track.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{track.artists}</p>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
{formatDuration(track.duration_ms || 0)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Albums */}
|
||||
{activeTab === "albums" && searchResults?.albums.map((album) => (
|
||||
<button
|
||||
key={album.id}
|
||||
type="button"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors"
|
||||
onClick={() => handleResultClick(album.external_urls)}
|
||||
>
|
||||
{album.images ? (
|
||||
<img src={album.images} alt="" className="w-12 h-12 rounded object-cover shrink-0" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded bg-muted shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{album.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{album.artists}</p>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
{album.total_tracks} tracks
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Artists */}
|
||||
{activeTab === "artists" && searchResults?.artists.map((artist) => (
|
||||
<button
|
||||
key={artist.id}
|
||||
type="button"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors"
|
||||
onClick={() => handleResultClick(artist.external_urls)}
|
||||
>
|
||||
{artist.images ? (
|
||||
<img src={artist.images} alt="" className="w-12 h-12 rounded-full object-cover shrink-0" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-muted shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{artist.name}</p>
|
||||
<p className="text-sm text-muted-foreground">Artist</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Playlists */}
|
||||
{activeTab === "playlists" && searchResults?.playlists.map((playlist) => (
|
||||
<button
|
||||
key={playlist.id}
|
||||
type="button"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors"
|
||||
onClick={() => handleResultClick(playlist.external_urls)}
|
||||
>
|
||||
{playlist.images ? (
|
||||
<img src={playlist.images} alt="" className="w-12 h-12 rounded object-cover shrink-0" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded bg-muted shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{playlist.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{playlist.owner} • {playlist.total_tracks} tracks
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Load More Button */}
|
||||
{hasMore[activeTab] && (
|
||||
<div className="flex justify-center pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{isLoadingMore ? (
|
||||
<>
|
||||
<Spinner />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
Load More
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ export function SettingsPage() {
|
||||
const settingsWithDefaults = await getSettingsWithDefaults();
|
||||
setSavedSettings(settingsWithDefaults);
|
||||
setTempSettings(settingsWithDefaults);
|
||||
// Save to localStorage so it persists on reload
|
||||
saveSettings(settingsWithDefaults);
|
||||
}
|
||||
};
|
||||
loadDefaults();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Download, FolderOpen, CheckCircle, XCircle, FileText, FileCheck, Globe, ImageDown } from "lucide-react";
|
||||
@@ -25,10 +24,13 @@ interface TrackInfoProps {
|
||||
checkingAvailability?: boolean;
|
||||
availability?: TrackAvailability;
|
||||
downloadingCover?: boolean;
|
||||
downloadedCover?: boolean;
|
||||
failedCover?: boolean;
|
||||
skippedCover?: boolean;
|
||||
onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string, playlistName?: string, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onOpenFolder: () => void;
|
||||
}
|
||||
|
||||
@@ -46,51 +48,26 @@ export function TrackInfo({
|
||||
checkingAvailability,
|
||||
availability,
|
||||
downloadingCover,
|
||||
downloadedCover,
|
||||
failedCover,
|
||||
skippedCover,
|
||||
onDownload,
|
||||
onDownloadLyrics,
|
||||
onCheckAvailability,
|
||||
onDownloadCover,
|
||||
onOpenFolder,
|
||||
}: TrackInfoProps) {
|
||||
const [isHoveringCover, setIsHoveringCover] = useState(false);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="px-6">
|
||||
<div className="flex gap-6 items-start">
|
||||
<div
|
||||
className="shrink-0 relative"
|
||||
onMouseEnter={() => setIsHoveringCover(true)}
|
||||
onMouseLeave={() => setIsHoveringCover(false)}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
{track.images && (
|
||||
<>
|
||||
<img
|
||||
src={track.images}
|
||||
alt={track.name}
|
||||
className="w-48 h-48 rounded-md shadow-lg object-cover"
|
||||
/>
|
||||
{isHoveringCover && onDownloadCover && (
|
||||
<div className="absolute inset-0 bg-black/50 rounded-md flex items-center justify-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDownloadCover(track.images, track.name, track.artists, track.album_name)}
|
||||
disabled={downloadingCover}
|
||||
>
|
||||
{downloadingCover ? <Spinner /> : <ImageDown className="h-5 w-5" />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download Cover</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<img
|
||||
src={track.images}
|
||||
alt={track.name}
|
||||
className="w-48 h-48 rounded-md shadow-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-4 min-w-0">
|
||||
@@ -136,7 +113,7 @@ export function TrackInfo({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => onDownloadLyrics(track.spotify_id!, track.name, track.artists, track.album_name)}
|
||||
onClick={() => onDownloadLyrics(track.spotify_id!, track.name, track.artists, track.album_name, track.album_artist, track.release_date, track.disc_number)}
|
||||
variant="outline"
|
||||
disabled={downloadingLyricsTrack === track.spotify_id}
|
||||
>
|
||||
@@ -158,6 +135,32 @@ export function TrackInfo({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{track.images && onDownloadCover && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => onDownloadCover(track.images, track.name, track.artists, track.album_name, undefined, undefined, track.spotify_id, track.album_artist, track.release_date, track.disc_number)}
|
||||
variant="outline"
|
||||
disabled={downloadingCover}
|
||||
>
|
||||
{downloadingCover ? (
|
||||
<Spinner />
|
||||
) : skippedCover ? (
|
||||
<FileCheck className="h-4 w-4 text-yellow-500" />
|
||||
) : downloadedCover ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
) : failedCover ? (
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
) : (
|
||||
<ImageDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download Cover</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{track.spotify_id && onCheckAvailability && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -50,9 +50,9 @@ interface TrackListProps {
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
onAlbumClick?: (album: { id: string; name: string; external_urls: string }) => void;
|
||||
onArtistClick?: (artist: { id: string; name: string; external_urls: string }) => void;
|
||||
@@ -339,7 +339,7 @@ export function TrackList({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() =>
|
||||
onDownloadLyrics(track.spotify_id!, track.name, track.artists, track.album_name, folderName, isArtistDiscography, startIndex + index + 1)
|
||||
onDownloadLyrics(track.spotify_id!, track.name, track.artists, track.album_name, folderName, isArtistDiscography, startIndex + index + 1, track.album_artist, track.release_date, track.disc_number)
|
||||
}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -369,7 +369,7 @@ export function TrackList({
|
||||
<Button
|
||||
onClick={() => {
|
||||
const trackId = track.spotify_id || `${track.name}-${track.artists}`;
|
||||
onDownloadCover(track.images, track.name, track.artists, track.album_name, folderName, isArtistDiscography, startIndex + index + 1, trackId);
|
||||
onDownloadCover(track.images, track.name, track.artists, track.album_name, folderName, isArtistDiscography, startIndex + index + 1, trackId, track.album_artist, track.release_date, track.disc_number);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
||||
@@ -23,7 +23,10 @@ export function useCover() {
|
||||
albumName?: string,
|
||||
playlistName?: string,
|
||||
position?: number,
|
||||
trackId?: string
|
||||
trackId?: string,
|
||||
albumArtist?: string,
|
||||
releaseDate?: string,
|
||||
discNumber?: number
|
||||
) => {
|
||||
if (!coverUrl) {
|
||||
toast.error("No cover URL found for this track");
|
||||
@@ -72,10 +75,14 @@ export function useCover() {
|
||||
cover_url: coverUrl,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
album_name: albumName || "",
|
||||
album_artist: albumArtist || "",
|
||||
release_date: releaseDate || "",
|
||||
output_dir: outputDir,
|
||||
filename_format: settings.filenameTemplate || "{title}",
|
||||
track_number: settings.trackNumber,
|
||||
position: position || 0,
|
||||
disc_number: discNumber || 0,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
@@ -145,12 +152,16 @@ export function useCover() {
|
||||
|
||||
// Replace forward slashes in template data values to prevent them from being interpreted as path separators
|
||||
const placeholder = "__SLASH_PLACEHOLDER__";
|
||||
// Determine if we should use album track number or sequential position
|
||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||
// Use track.track_number for album context, otherwise use sequential position (consistent with track download)
|
||||
const trackPosition = useAlbumTrackNumber ? (track.track_number || i + 1) : (i + 1);
|
||||
// Build output path using template system
|
||||
const templateData: TemplateData = {
|
||||
artist: track.artists?.replace(/\//g, placeholder),
|
||||
album: track.album_name?.replace(/\//g, placeholder),
|
||||
title: track.name?.replace(/\//g, placeholder),
|
||||
track: i + 1,
|
||||
track: trackPosition,
|
||||
playlist: playlistName?.replace(/\//g, placeholder),
|
||||
};
|
||||
|
||||
@@ -176,10 +187,14 @@ export function useCover() {
|
||||
cover_url: track.images,
|
||||
track_name: track.name,
|
||||
artist_name: track.artists,
|
||||
album_name: track.album_name,
|
||||
album_artist: track.album_artist,
|
||||
release_date: track.release_date,
|
||||
output_dir: outputDir,
|
||||
filename_format: settings.filenameTemplate || "{title}",
|
||||
track_number: settings.trackNumber,
|
||||
position: i + 1,
|
||||
position: trackPosition,
|
||||
disc_number: track.disc_number,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
|
||||
@@ -21,7 +21,10 @@ export function useLyrics() {
|
||||
artistName: string,
|
||||
albumName?: string,
|
||||
playlistName?: string,
|
||||
position?: number
|
||||
position?: number,
|
||||
albumArtist?: string,
|
||||
releaseDate?: string,
|
||||
discNumber?: number
|
||||
) => {
|
||||
if (!spotifyId) {
|
||||
toast.error("No Spotify ID found for this track");
|
||||
@@ -71,11 +74,15 @@ export function useLyrics() {
|
||||
spotify_id: spotifyId,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
release_date: releaseDate,
|
||||
output_dir: outputDir,
|
||||
filename_format: settings.filenameTemplate || "{title}",
|
||||
track_number: settings.trackNumber,
|
||||
position: position || 0,
|
||||
use_album_track_number: useAlbumTrackNumber,
|
||||
disc_number: discNumber,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
@@ -126,7 +133,8 @@ export function useLyrics() {
|
||||
let skipped = 0;
|
||||
const total = tracksWithSpotifyId.length;
|
||||
|
||||
for (const track of tracksWithSpotifyId) {
|
||||
for (let i = 0; i < tracksWithSpotifyId.length; i++) {
|
||||
const track = tracksWithSpotifyId[i];
|
||||
if (stopBulkDownloadRef.current) {
|
||||
toast.info("Lyrics download stopped by user");
|
||||
break;
|
||||
@@ -142,12 +150,18 @@ export function useLyrics() {
|
||||
|
||||
// Replace forward slashes in template data values to prevent them from being interpreted as path separators
|
||||
const placeholder = "__SLASH_PLACEHOLDER__";
|
||||
|
||||
// Determine if we should use album track number or sequential position
|
||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||
// Use track.track_number for album context, otherwise use sequential position (consistent with track download)
|
||||
const trackPosition = useAlbumTrackNumber ? (track.track_number || i + 1) : (i + 1);
|
||||
|
||||
// Build output path using template system
|
||||
const templateData: TemplateData = {
|
||||
artist: track.artists?.replace(/\//g, placeholder),
|
||||
album: track.album_name?.replace(/\//g, placeholder),
|
||||
title: track.name?.replace(/\//g, placeholder),
|
||||
track: track.track_number,
|
||||
track: trackPosition,
|
||||
playlist: playlistName?.replace(/\//g, placeholder),
|
||||
};
|
||||
|
||||
@@ -169,17 +183,19 @@ export function useLyrics() {
|
||||
}
|
||||
}
|
||||
|
||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||
|
||||
const response = await downloadLyrics({
|
||||
spotify_id: id,
|
||||
track_name: track.name,
|
||||
artist_name: track.artists,
|
||||
album_name: track.album_name,
|
||||
album_artist: track.album_artist,
|
||||
release_date: track.release_date,
|
||||
output_dir: outputDir,
|
||||
filename_format: settings.filenameTemplate || "{title}",
|
||||
track_number: settings.trackNumber,
|
||||
position: track.track_number || 0,
|
||||
position: trackPosition,
|
||||
use_album_track_number: useAlbumTrackNumber,
|
||||
disc_number: track.disc_number,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: "Google Sans Flex", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-family: "Google Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
code, pre, .font-mono {
|
||||
font-family: "Google Sans Code", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
|
||||
|
||||
@@ -112,7 +112,7 @@ export const FONT_OPTIONS: { value: FontFamily; label: string; fontFamily: strin
|
||||
{ value: "dm-sans", label: "DM Sans", fontFamily: '"DM Sans", system-ui, sans-serif' },
|
||||
{ value: "figtree", label: "Figtree", fontFamily: '"Figtree", system-ui, sans-serif' },
|
||||
{ value: "geist-sans", label: "Geist Sans", fontFamily: '"Geist", system-ui, sans-serif' },
|
||||
{ value: "google-sans", label: "Google Sans Flex", fontFamily: '"Google Sans Flex", system-ui, sans-serif' },
|
||||
{ value: "google-sans", label: "Google Sans", fontFamily: '"Google Sans", system-ui, sans-serif' },
|
||||
{ value: "inter", label: "Inter", fontFamily: '"Inter", system-ui, sans-serif' },
|
||||
{ value: "jetbrains-mono", label: "JetBrains Mono", fontFamily: '"JetBrains Mono", ui-monospace, monospace' },
|
||||
{ value: "manrope", label: "Manrope", fontFamily: '"Manrope", system-ui, sans-serif' },
|
||||
|
||||
@@ -185,11 +185,15 @@ export interface LyricsDownloadRequest {
|
||||
spotify_id: string;
|
||||
track_name: string;
|
||||
artist_name: string;
|
||||
album_name?: string;
|
||||
album_artist?: string;
|
||||
release_date?: string;
|
||||
output_dir?: string;
|
||||
filename_format?: string;
|
||||
track_number?: boolean;
|
||||
position?: number;
|
||||
use_album_track_number?: boolean;
|
||||
disc_number?: number;
|
||||
}
|
||||
|
||||
export interface LyricsDownloadResponse {
|
||||
@@ -214,10 +218,14 @@ export interface CoverDownloadRequest {
|
||||
cover_url: string;
|
||||
track_name: string;
|
||||
artist_name: string;
|
||||
album_name?: string;
|
||||
album_artist?: string;
|
||||
release_date?: string;
|
||||
output_dir?: string;
|
||||
filename_format?: string;
|
||||
track_number?: boolean;
|
||||
position?: number;
|
||||
disc_number?: number;
|
||||
}
|
||||
|
||||
export interface CoverDownloadResponse {
|
||||
|
||||
Reference in New Issue
Block a user