music player + upd index
This commit is contained in:
parent
4715acaaf3
commit
96257a0188
@ -12,5 +12,9 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.1.1",
|
"@sveltejs/vite-plugin-svelte": "^6.1.1",
|
||||||
"svelte": "^5.38.1",
|
"svelte": "^5.38.1",
|
||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-svelte": "^0.541.0",
|
||||||
|
"music-metadata": "^11.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
pnpm-lock.yaml
generated
120
pnpm-lock.yaml
generated
@ -7,6 +7,13 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
lucide-svelte:
|
||||||
|
specifier: ^0.541.0
|
||||||
|
version: 0.541.0(svelte@5.38.3)
|
||||||
|
music-metadata:
|
||||||
|
specifier: ^11.8.3
|
||||||
|
version: 11.8.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sveltejs/vite-plugin-svelte':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
@ -20,6 +27,12 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@borewit/text-codec@0.1.1':
|
||||||
|
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
|
||||||
|
|
||||||
|
'@borewit/text-codec@0.2.0':
|
||||||
|
resolution: {integrity: sha512-X999CKBxGwX8wW+4gFibsbiNdwqmdQEXmUejIWaIqdrHBgS5ARIOOeyiQbHjP9G58xVEPcuvP6VwwH3A0OFTOA==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.25.9':
|
'@esbuild/aix-ppc64@0.25.9':
|
||||||
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -312,6 +325,13 @@ packages:
|
|||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
vite: ^6.3.0 || ^7.0.0
|
vite: ^6.3.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@tokenizer/inflate@0.2.7':
|
||||||
|
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@tokenizer/token@0.3.0':
|
||||||
|
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@ -332,6 +352,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
content-type@1.0.5:
|
||||||
|
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@ -365,11 +389,21 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fflate@0.8.2:
|
||||||
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
|
||||||
|
file-type@21.0.0:
|
||||||
|
resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
is-reference@3.0.3:
|
is-reference@3.0.3:
|
||||||
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
||||||
|
|
||||||
@ -380,12 +414,25 @@ packages:
|
|||||||
locate-character@3.0.0:
|
locate-character@3.0.0:
|
||||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||||
|
|
||||||
|
lucide-svelte@0.541.0:
|
||||||
|
resolution: {integrity: sha512-Jk+LiOYDl62R/0nWkG1s5XL2k6LHmPq3wUfiJ6qtBhb8jGefB4PU10x5HJrAihwaKqVc2vH5wjKMELGjHJenEQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||||
|
|
||||||
magic-string@0.30.18:
|
magic-string@0.30.18:
|
||||||
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
|
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
|
||||||
|
|
||||||
|
media-typer@1.1.0:
|
||||||
|
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
music-metadata@11.8.3:
|
||||||
|
resolution: {integrity: sha512-Tgiv4MlCgDb6XzelziB1mmL2xeoHls0KTpCm3Z3qr+LfF4mBEpkuc5vNrc927IT5+S5fv+vzStfI+HYC0igDpA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@ -411,6 +458,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
strtok3@10.3.4:
|
||||||
|
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
svelte@5.38.3:
|
svelte@5.38.3:
|
||||||
resolution: {integrity: sha512-ldbPzKdjUy7IALMBn15jzBM/TNxdXMxKeQZ538zzdABUjLg7e7/OIwnlaMQ+OR6s91W7DbDmJYjxHThHH7r9xA==}
|
resolution: {integrity: sha512-ldbPzKdjUy7IALMBn15jzBM/TNxdXMxKeQZ538zzdABUjLg7e7/OIwnlaMQ+OR6s91W7DbDmJYjxHThHH7r9xA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -419,6 +470,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
token-types@6.1.1:
|
||||||
|
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
uint8array-extras@1.5.0:
|
||||||
|
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
vite@7.1.3:
|
vite@7.1.3:
|
||||||
resolution: {integrity: sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==}
|
resolution: {integrity: sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@ -472,6 +531,10 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@borewit/text-codec@0.1.1': {}
|
||||||
|
|
||||||
|
'@borewit/text-codec@0.2.0': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.25.9':
|
'@esbuild/aix-ppc64@0.25.9':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -655,6 +718,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@tokenizer/inflate@0.2.7':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.1
|
||||||
|
fflate: 0.8.2
|
||||||
|
token-types: 6.1.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@tokenizer/token@0.3.0': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
acorn@8.15.0: {}
|
acorn@8.15.0: {}
|
||||||
@ -665,6 +738,8 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
content-type@1.0.5: {}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@ -710,9 +785,22 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
fflate@0.8.2: {}
|
||||||
|
|
||||||
|
file-type@21.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@tokenizer/inflate': 0.2.7
|
||||||
|
strtok3: 10.3.4
|
||||||
|
token-types: 6.1.1
|
||||||
|
uint8array-extras: 1.5.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
is-reference@3.0.3:
|
is-reference@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -721,12 +809,32 @@ snapshots:
|
|||||||
|
|
||||||
locate-character@3.0.0: {}
|
locate-character@3.0.0: {}
|
||||||
|
|
||||||
|
lucide-svelte@0.541.0(svelte@5.38.3):
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.38.3
|
||||||
|
|
||||||
magic-string@0.30.18:
|
magic-string@0.30.18:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
media-typer@1.1.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
music-metadata@11.8.3:
|
||||||
|
dependencies:
|
||||||
|
'@borewit/text-codec': 0.2.0
|
||||||
|
'@tokenizer/token': 0.3.0
|
||||||
|
content-type: 1.0.5
|
||||||
|
debug: 4.4.1
|
||||||
|
file-type: 21.0.0
|
||||||
|
media-typer: 1.1.0
|
||||||
|
strtok3: 10.3.4
|
||||||
|
token-types: 6.1.1
|
||||||
|
uint8array-extras: 1.5.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@ -767,6 +875,10 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
strtok3@10.3.4:
|
||||||
|
dependencies:
|
||||||
|
'@tokenizer/token': 0.3.0
|
||||||
|
|
||||||
svelte@5.38.3:
|
svelte@5.38.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@ -789,6 +901,14 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
token-types@6.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@borewit/text-codec': 0.1.1
|
||||||
|
'@tokenizer/token': 0.3.0
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
uint8array-extras@1.5.0: {}
|
||||||
|
|
||||||
vite@7.1.3:
|
vite@7.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.9
|
esbuild: 0.25.9
|
||||||
|
|||||||
BIN
public/music/jifmachinecd.flac
Normal file
BIN
public/music/jifmachinecd.flac
Normal file
Binary file not shown.
BIN
public/music/mgxleepe.flac
Normal file
BIN
public/music/mgxleepe.flac
Normal file
Binary file not shown.
@ -1,4 +1,23 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import AudioPlayer from './lib/AudioPlayer.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let dayCounter = '0.00000000';
|
||||||
|
|
||||||
|
function updateCounter() {
|
||||||
|
const startDate = new Date('2011-09-19T00:00:00');
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - startDate.getTime();
|
||||||
|
const years = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
||||||
|
dayCounter = years.toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateCounter();
|
||||||
|
const interval = setInterval(updateCounter, 10);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
const contacts = [
|
const contacts = [
|
||||||
{ label: 'email', href: 'mailto:lain@iwakurahome.ru', text: 'lain@iwakurahome.ru' },
|
{ label: 'email', href: 'mailto:lain@iwakurahome.ru', text: 'lain@iwakurahome.ru' },
|
||||||
{ label: 'telegram', href: 'https://t.me/systemxplore', text: '@systemxplore' },
|
{ label: 'telegram', href: 'https://t.me/systemxplore', text: '@systemxplore' },
|
||||||
@ -48,7 +67,9 @@
|
|||||||
<div class="aperture"></div>
|
<div class="aperture"></div>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="head-wrap">
|
<div class="head-wrap">
|
||||||
<div class="brand">Lain <span class="handle">@systemxplore</span></div>
|
<div class="brand">
|
||||||
|
Lain <span class="handle">@systemxplore</span> {dayCounter}
|
||||||
|
</div>
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a class="{location.pathname === '/' ? 'current' : ''}" href="/">whoami</a>
|
<a class="{location.pathname === '/' ? 'current' : ''}" href="/">whoami</a>
|
||||||
</nav>
|
</nav>
|
||||||
@ -109,6 +130,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AudioPlayer />
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
|
|||||||
446
src/lib/AudioPlayer.css
Normal file
446
src/lib/AudioPlayer.css
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
.audio-player {
|
||||||
|
background: var(--bg-1);
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: Fantasque, VT323, ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state, .error-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--bg-1);
|
||||||
|
border-top: 3px solid var(--green);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--bg-1);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--green);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-badge {
|
||||||
|
background: var(--green);
|
||||||
|
color: var(--bg-0);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-counter {
|
||||||
|
background: var(--bg-2);
|
||||||
|
color: var(--green-2);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
border: 1px solid var(--green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist {
|
||||||
|
color: var(--green-2);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album, .year {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-info {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: Fantasque, monospace;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
background: var(--bg-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-glow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed var(--green);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-placeholder :global(svg) {
|
||||||
|
opacity: 0.7;
|
||||||
|
color: var(--green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn, .playlist-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: var(--green);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover, .playlist-btn:hover {
|
||||||
|
background: var(--green);
|
||||||
|
color: var(--bg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:disabled, .playlist-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:disabled:hover, .playlist-btn:disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
background: var(--green);
|
||||||
|
color: var(--bg-0);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:hover {
|
||||||
|
background: var(--green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-control {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
color: var(--green-2);
|
||||||
|
font-family: Fantasque, monospace;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time {
|
||||||
|
color: var(--green);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-time {
|
||||||
|
color: var(--green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-remaining {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
font-family: Fantasque, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
color: var(--green);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-btn:hover {
|
||||||
|
background: var(--green);
|
||||||
|
color: var(--bg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
width: 80px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-1);
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--green);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--green);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-percentage {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #666;
|
||||||
|
font-family: Fantasque, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container:focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--green);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container:hover .progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-1);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--green), var(--green-2));
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-top: 1px solid var(--green);
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header {
|
||||||
|
color: var(--green);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-items {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--green);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-bottom: 1px solid rgba(0, 255, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item:hover, .playlist-item:focus {
|
||||||
|
background: var(--bg-1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item:focus {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item.active {
|
||||||
|
background: rgba(0, 255, 0, 0.1);
|
||||||
|
border-left: 3px solid var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-number {
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--green-2);
|
||||||
|
font-family: Fantasque, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
color: var(--green);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-artist {
|
||||||
|
color: var(--green-2);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-duration {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: Fantasque, monospace;
|
||||||
|
}
|
||||||
331
src/lib/AudioPlayer.svelte
Normal file
331
src/lib/AudioPlayer.svelte
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
<script>
|
||||||
|
import { parseBuffer } from 'music-metadata';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Play, Pause, Volume2, Volume1, VolumeX, Music, SkipForward, SkipBack, List } from 'lucide-svelte';
|
||||||
|
import './AudioPlayer.css';
|
||||||
|
|
||||||
|
let audioElement;
|
||||||
|
let isPlaying = false;
|
||||||
|
let currentTime = 0;
|
||||||
|
let duration = 0;
|
||||||
|
let volume = 0.7;
|
||||||
|
let loading = true;
|
||||||
|
let loadingProgress = 0;
|
||||||
|
let error = null;
|
||||||
|
let showPlaylist = false;
|
||||||
|
|
||||||
|
let tracks = [];
|
||||||
|
let currentTrackIndex = 0;
|
||||||
|
let currentTrack = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadTracks();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadTracks() {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (loading) {
|
||||||
|
error = 'Loading timeout';
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingProgress = 10;
|
||||||
|
|
||||||
|
const trackFiles = ['mgxleepe.flac', 'jifmachinecd.flac'];
|
||||||
|
tracks = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < trackFiles.length; i++) {
|
||||||
|
const filename = trackFiles[i];
|
||||||
|
loadingProgress = 20 + (i * 60 / trackFiles.length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/music/${filename}`);
|
||||||
|
if (!response.ok) continue;
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const metadata = await parseBuffer(new Uint8Array(arrayBuffer));
|
||||||
|
|
||||||
|
tracks.push({
|
||||||
|
filename,
|
||||||
|
src: `/music/${filename}`,
|
||||||
|
metadata,
|
||||||
|
title: metadata.common.title || filename.replace('.flac', ''),
|
||||||
|
artist: metadata.common.artist || 'Unknown Artist',
|
||||||
|
album: metadata.common.album,
|
||||||
|
year: metadata.common.year,
|
||||||
|
duration: metadata.format.duration
|
||||||
|
});
|
||||||
|
} catch (trackError) {
|
||||||
|
console.warn(`Failed to load ${filename}:`, trackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracks.length === 0) {
|
||||||
|
throw new Error('No audio files found');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingProgress = 90;
|
||||||
|
currentTrack = tracks[0];
|
||||||
|
loadingProgress = 100;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loading = false;
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading tracks:', err);
|
||||||
|
error = err.message || 'Failed to load tracks';
|
||||||
|
loading = false;
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePlay() {
|
||||||
|
if (isPlaying) {
|
||||||
|
audioElement.pause();
|
||||||
|
} else {
|
||||||
|
audioElement.play();
|
||||||
|
}
|
||||||
|
isPlaying = !isPlaying;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextTrack() {
|
||||||
|
if (tracks.length === 0) return;
|
||||||
|
currentTrackIndex = (currentTrackIndex + 1) % tracks.length;
|
||||||
|
switchToTrack(currentTrackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevTrack() {
|
||||||
|
if (tracks.length === 0) return;
|
||||||
|
currentTrackIndex = currentTrackIndex === 0 ? tracks.length - 1 : currentTrackIndex - 1;
|
||||||
|
switchToTrack(currentTrackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToTrack(index) {
|
||||||
|
if (index < 0 || index >= tracks.length) return;
|
||||||
|
|
||||||
|
const wasPlaying = isPlaying;
|
||||||
|
if (isPlaying) {
|
||||||
|
audioElement.pause();
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTrackIndex = index;
|
||||||
|
currentTrack = tracks[index];
|
||||||
|
currentTime = 0;
|
||||||
|
|
||||||
|
if (wasPlaying) {
|
||||||
|
setTimeout(() => {
|
||||||
|
audioElement.play();
|
||||||
|
isPlaying = true;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTime() {
|
||||||
|
currentTime = audioElement.currentTime;
|
||||||
|
duration = audioElement.duration || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function seek(event) {
|
||||||
|
const rect = event.target.getBoundingClientRect();
|
||||||
|
const percent = (event.clientX - rect.left) / rect.width;
|
||||||
|
audioElement.currentTime = percent * duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
if (!seconds || isNaN(seconds)) return '0:00';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVolumeChange() {
|
||||||
|
audioElement.volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
try {
|
||||||
|
const uint8Array = new Uint8Array(buffer);
|
||||||
|
const binaryString = uint8Array.reduce((str, byte) => str + String.fromCharCode(byte), '');
|
||||||
|
return btoa(binaryString);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting image to base64:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="audio-player">
|
||||||
|
<audio
|
||||||
|
bind:this={audioElement}
|
||||||
|
src={currentTrack?.src}
|
||||||
|
on:timeupdate={updateTime}
|
||||||
|
on:loadedmetadata={updateTime}
|
||||||
|
on:ended={nextTrack}
|
||||||
|
></audio>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">Loading tracks... {loadingProgress}%</div>
|
||||||
|
<div class="loading-bar">
|
||||||
|
<div class="loading-fill" style="width: {loadingProgress}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="error-state">
|
||||||
|
<div class="error-icon">⚠</div>
|
||||||
|
<div class="error-text">{error}</div>
|
||||||
|
</div>
|
||||||
|
{:else if currentTrack}
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="track-info">
|
||||||
|
<div class="title-row">
|
||||||
|
<div class="title">{currentTrack.title}</div>
|
||||||
|
<div class="format-badge">FLAC</div>
|
||||||
|
<div class="track-counter">{currentTrackIndex + 1}/{tracks.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="artist">{currentTrack.artist}</div>
|
||||||
|
{#if currentTrack.album}
|
||||||
|
<div class="album">[Album] {currentTrack.album}</div>
|
||||||
|
{/if}
|
||||||
|
{#if currentTrack.year}
|
||||||
|
<div class="year">[Year] {currentTrack.year}</div>
|
||||||
|
{/if}
|
||||||
|
{#if currentTrack.metadata.format}
|
||||||
|
<div class="tech-info">
|
||||||
|
{Math.round(currentTrack.metadata.format.sampleRate / 1000)}kHz • {currentTrack.metadata.format.bitsPerSample}bit • {currentTrack.metadata.format.bitrate ? Math.round(currentTrack.metadata.format.bitrate / 1000) + 'kbps' : 'Lossless'}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if currentTrack.metadata.common.picture && currentTrack.metadata.common.picture[0]}
|
||||||
|
{@const base64Image = arrayBufferToBase64(currentTrack.metadata.common.picture[0].data)}
|
||||||
|
{#if base64Image}
|
||||||
|
<div class="artwork">
|
||||||
|
<img
|
||||||
|
src={`data:${currentTrack.metadata.common.picture[0].format};base64,${base64Image}`}
|
||||||
|
alt="Album artwork"
|
||||||
|
class="artwork-img"
|
||||||
|
/>
|
||||||
|
<div class="artwork-glow"></div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="artwork-placeholder">
|
||||||
|
<Music size={24} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="artwork-placeholder">
|
||||||
|
<Music size={24} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button class="control-btn" on:click={prevTrack} disabled={loading || error || tracks.length <= 1}>
|
||||||
|
<SkipBack size={14} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="play-btn {isPlaying ? 'playing' : ''}" on:click={togglePlay} disabled={loading || error}>
|
||||||
|
<div class="btn-content">
|
||||||
|
{#if isPlaying}
|
||||||
|
<Pause size={16} />
|
||||||
|
{:else}
|
||||||
|
<Play size={16} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="control-btn" on:click={nextTrack} disabled={loading || error || tracks.length <= 1}>
|
||||||
|
<SkipForward size={14} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="time-control">
|
||||||
|
<div class="time-display">
|
||||||
|
<span class="current-time">{formatTime(currentTime)}</span>
|
||||||
|
<span class="separator">/</span>
|
||||||
|
<span class="total-time">{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
{#if duration > 0}
|
||||||
|
<div class="time-remaining">-{formatTime(duration - currentTime)}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="volume-control">
|
||||||
|
<button class="volume-btn" on:click={() => volume = volume > 0 ? 0 : 0.7}>
|
||||||
|
{#if volume === 0}
|
||||||
|
<VolumeX size={14} />
|
||||||
|
{:else if volume < 0.5}
|
||||||
|
<Volume1 size={14} />
|
||||||
|
{:else}
|
||||||
|
<Volume2 size={14} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<div class="volume-slider-container">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
bind:value={volume}
|
||||||
|
on:input={onVolumeChange}
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
class="volume-slider"
|
||||||
|
/>
|
||||||
|
<div class="volume-percentage">{Math.round(volume * 100)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="playlist-btn" on:click={() => showPlaylist = !showPlaylist} disabled={loading || error || tracks.length <= 1}>
|
||||||
|
<List size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="progress-container"
|
||||||
|
on:click={seek}
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && seek(e)}
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Seek audio position"
|
||||||
|
aria-valuenow={duration ? Math.round((currentTime / duration) * 100) : 0}
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
style="width: {duration ? (currentTime / duration) * 100 : 0}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showPlaylist && tracks.length > 1}
|
||||||
|
<div class="playlist">
|
||||||
|
<div class="playlist-header">Playlist ({tracks.length} tracks)</div>
|
||||||
|
<div class="playlist-items">
|
||||||
|
{#each tracks as track, index}
|
||||||
|
<div
|
||||||
|
class="playlist-item {index === currentTrackIndex ? 'active' : ''}"
|
||||||
|
on:click={() => switchToTrack(index)}
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && switchToTrack(index)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Switch to track {track.title}"
|
||||||
|
>
|
||||||
|
<div class="track-number">{index + 1}</div>
|
||||||
|
<div class="track-details">
|
||||||
|
<div class="track-title">{track.title}</div>
|
||||||
|
<div class="track-artist">{track.artist}</div>
|
||||||
|
</div>
|
||||||
|
<div class="track-duration">{formatTime(track.duration || 0)}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Loading…
x
Reference in New Issue
Block a user