[web] Fix styles to comply with Bulma 1.0

This commit is contained in:
Alain Nussbaumer 2025-02-04 22:00:48 +01:00
parent b2fbbd3fa0
commit 8140e008f0
117 changed files with 3064 additions and 3606 deletions

View File

@ -65,12 +65,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.3"
"@babel/types": "^7.26.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -80,9 +80,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@ -526,13 +526,13 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
"integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.5",
"@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@ -541,9 +541,9 @@
}
},
"node_modules/@eslint/core": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
"integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -578,9 +578,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -588,9 +588,9 @@
}
},
"node_modules/@eslint/object-schema": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
"integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -598,12 +598,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
"integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
"integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.10.0",
"levn": "^0.4.1"
},
"engines": {
@ -840,9 +841,9 @@
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz",
"integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -861,25 +862,25 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.0",
"@parcel/watcher-darwin-arm64": "2.5.0",
"@parcel/watcher-darwin-x64": "2.5.0",
"@parcel/watcher-freebsd-x64": "2.5.0",
"@parcel/watcher-linux-arm-glibc": "2.5.0",
"@parcel/watcher-linux-arm-musl": "2.5.0",
"@parcel/watcher-linux-arm64-glibc": "2.5.0",
"@parcel/watcher-linux-arm64-musl": "2.5.0",
"@parcel/watcher-linux-x64-glibc": "2.5.0",
"@parcel/watcher-linux-x64-musl": "2.5.0",
"@parcel/watcher-win32-arm64": "2.5.0",
"@parcel/watcher-win32-ia32": "2.5.0",
"@parcel/watcher-win32-x64": "2.5.0"
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz",
"integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
@ -898,9 +899,9 @@
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz",
"integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
@ -919,9 +920,9 @@
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz",
"integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
@ -940,9 +941,9 @@
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz",
"integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
@ -961,9 +962,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz",
"integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
@ -982,9 +983,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz",
"integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
@ -1003,9 +1004,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz",
"integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
@ -1024,9 +1025,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz",
"integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
@ -1045,9 +1046,9 @@
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz",
"integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
@ -1066,9 +1067,9 @@
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz",
"integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
@ -1087,9 +1088,9 @@
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz",
"integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
@ -1108,9 +1109,9 @@
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz",
"integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
@ -1129,9 +1130,9 @@
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz",
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
@ -1173,9 +1174,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz",
"integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.2.tgz",
"integrity": "sha512-6Fyg9yQbwJR+ykVdT9sid1oc2ewejS6h4wzQltmJfSW53N60G/ah9pngXGANdy9/aaE/TcUFpWosdm7JXS1WTQ==",
"cpu": [
"arm"
],
@ -1187,9 +1188,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz",
"integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.2.tgz",
"integrity": "sha512-K5GfWe+vtQ3kyEbihrimM38UgX57UqHp+oME7X/EX9Im6suwZfa7Hsr8AtzbJvukTpwMGs+4s29YMSO3rwWtsw==",
"cpu": [
"arm64"
],
@ -1201,9 +1202,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz",
"integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.2.tgz",
"integrity": "sha512-PSN58XG/V/tzqDb9kDGutUruycgylMlUE59f40ny6QIRNsTEIZsrNQTJKUN2keMMSmlzgunMFqyaGLmly39sug==",
"cpu": [
"arm64"
],
@ -1215,9 +1216,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz",
"integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.2.tgz",
"integrity": "sha512-gQhK788rQJm9pzmXyfBB84VHViDERhAhzGafw+E5mUpnGKuxZGkMVDa3wgDFKT6ukLC5V7QTifzsUKdNVxp5qQ==",
"cpu": [
"x64"
],
@ -1229,9 +1230,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz",
"integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.2.tgz",
"integrity": "sha512-eiaHgQwGPpxLC3+zTAcdKl4VsBl3r0AiJOd1Um/ArEzAjN/dbPK1nROHrVkdnoE6p7Svvn04w3f/jEZSTVHunA==",
"cpu": [
"arm64"
],
@ -1243,9 +1244,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz",
"integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.2.tgz",
"integrity": "sha512-lhdiwQ+jf8pewYOTG4bag0Qd68Jn1v2gO1i0mTuiD+Qkt5vNfHVK/jrT7uVvycV8ZchlzXp5HDVmhpzjC6mh0g==",
"cpu": [
"x64"
],
@ -1257,9 +1258,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz",
"integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.2.tgz",
"integrity": "sha512-lfqTpWjSvbgQP1vqGTXdv+/kxIznKXZlI109WkIFPbud41bjigjNmOAAKoazmRGx+k9e3rtIdbq2pQZPV1pMig==",
"cpu": [
"arm"
],
@ -1271,9 +1272,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz",
"integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.2.tgz",
"integrity": "sha512-RGjqULqIurqqv+NJTyuPgdZhka8ImMLB32YwUle2BPTDqDoXNgwFjdjQC59FbSk08z0IqlRJjrJ0AvDQ5W5lpw==",
"cpu": [
"arm"
],
@ -1285,9 +1286,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz",
"integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.2.tgz",
"integrity": "sha512-ZvkPiheyXtXlFqHpsdgscx+tZ7hoR59vOettvArinEspq5fxSDSgfF+L5wqqJ9R4t+n53nyn0sKxeXlik7AY9Q==",
"cpu": [
"arm64"
],
@ -1299,9 +1300,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz",
"integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.2.tgz",
"integrity": "sha512-UlFk+E46TZEoxD9ufLKDBzfSG7Ki03fo6hsNRRRHF+KuvNZ5vd1RRVQm8YZlGsjcJG8R252XFK0xNPay+4WV7w==",
"cpu": [
"arm64"
],
@ -1313,9 +1314,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz",
"integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.2.tgz",
"integrity": "sha512-hJhfsD9ykx59jZuuoQgYT1GEcNNi3RCoEmbo5OGfG8RlHOiVS7iVNev9rhLKh7UBYq409f4uEw0cclTXx8nh8Q==",
"cpu": [
"loong64"
],
@ -1327,9 +1328,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz",
"integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.2.tgz",
"integrity": "sha512-g/O5IpgtrQqPegvqopvmdCF9vneLE7eqYfdPWW8yjPS8f63DNam3U4ARL1PNNB64XHZDHKpvO2Giftf43puB8Q==",
"cpu": [
"ppc64"
],
@ -1341,9 +1342,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz",
"integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.2.tgz",
"integrity": "sha512-bSQijDC96M6PuooOuXHpvXUYiIwsnDmqGU8+br2U7iPoykNi9JtMUpN7K6xml29e0evK0/g0D1qbAUzWZFHY5Q==",
"cpu": [
"riscv64"
],
@ -1355,9 +1356,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz",
"integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.2.tgz",
"integrity": "sha512-49TtdeVAsdRuiUHXPrFVucaP4SivazetGUVH8CIxVsNsaPHV4PFkpLmH9LeqU/R4Nbgky9lzX5Xe1NrzLyraVA==",
"cpu": [
"s390x"
],
@ -1369,9 +1370,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz",
"integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.2.tgz",
"integrity": "sha512-j+jFdfOycLIQ7FWKka9Zd3qvsIyugg5LeZuHF6kFlXo6MSOc6R1w37YUVy8VpAKd81LMWGi5g9J25P09M0SSIw==",
"cpu": [
"x64"
],
@ -1383,9 +1384,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz",
"integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.2.tgz",
"integrity": "sha512-aDPHyM/D2SpXfSNCVWCxyHmOqN9qb7SWkY1+vaXqMNMXslZYnwh9V/UCudl6psyG0v6Ukj7pXanIpfZwCOEMUg==",
"cpu": [
"x64"
],
@ -1397,9 +1398,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz",
"integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.2.tgz",
"integrity": "sha512-LQRkCyUBnAo7r8dbEdtNU08EKLCJMgAk2oP5H3R7BnUlKLqgR3dUjrLBVirmc1RK6U6qhtDw29Dimeer8d5hzQ==",
"cpu": [
"arm64"
],
@ -1411,9 +1412,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz",
"integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.2.tgz",
"integrity": "sha512-wt8OhpQUi6JuPFkm1wbVi1BByeag87LDFzeKSXzIdGcX4bMLqORTtKxLoCbV57BHYNSUSOKlSL4BYYUghainYA==",
"cpu": [
"ia32"
],
@ -1425,9 +1426,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz",
"integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.2.tgz",
"integrity": "sha512-rUrqINax0TvrPBXrFKg0YbQx18NpPN3NNrgmaao9xRNbTwek7lOXObhx8tQy8gelmQ/gLaGy1WptpU2eKJZImg==",
"cpu": [
"x64"
],
@ -1795,9 +1796,9 @@
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
@ -1974,19 +1975,19 @@
}
},
"node_modules/eslint": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
"integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.9.0",
"@eslint/core": "^0.10.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.17.0",
"@eslint/plugin-kit": "^0.2.3",
"@eslint/js": "9.19.0",
"@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.1",
@ -2207,9 +2208,9 @@
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2217,7 +2218,7 @@
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.4"
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
@ -2251,9 +2252,9 @@
"license": "MIT"
},
"node_modules/fastq": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"dev": true,
"license": "ISC",
"dependencies": {
@ -2427,9 +2428,9 @@
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2747,18 +2748,25 @@
}
},
"node_modules/mlly": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz",
"integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"pathe": "^1.1.2",
"pkg-types": "^1.2.1",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"ufo": "^1.5.4"
}
},
"node_modules/mlly/node_modules/pathe": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
"integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
"dev": true,
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -2922,9 +2930,9 @@
}
},
"node_modules/pinia": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
"integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
@ -2944,21 +2952,28 @@
}
},
"node_modules/pkg-types": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.0.tgz",
"integrity": "sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
"mlly": "^1.7.3",
"pathe": "^1.1.2"
"mlly": "^1.7.4",
"pathe": "^2.0.1"
}
},
"node_modules/pkg-types/node_modules/pathe": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
"integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
"dev": true,
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"funding": [
{
"type": "opencollective",
@ -2975,7 +2990,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -3061,13 +3076,13 @@
"license": "MIT"
},
"node_modules/readdirp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
"integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
@ -3102,9 +3117,9 @@
}
},
"node_modules/rollup": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz",
"integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.2.tgz",
"integrity": "sha512-sBDUoxZEaqLu9QeNalL8v3jw6WjPku4wfZGyTU7l7m1oC+rpRihXc/n/H+4148ZkGz5Xli8CHMns//fFGKvpIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3118,25 +3133,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.29.1",
"@rollup/rollup-android-arm64": "4.29.1",
"@rollup/rollup-darwin-arm64": "4.29.1",
"@rollup/rollup-darwin-x64": "4.29.1",
"@rollup/rollup-freebsd-arm64": "4.29.1",
"@rollup/rollup-freebsd-x64": "4.29.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.29.1",
"@rollup/rollup-linux-arm-musleabihf": "4.29.1",
"@rollup/rollup-linux-arm64-gnu": "4.29.1",
"@rollup/rollup-linux-arm64-musl": "4.29.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.29.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.29.1",
"@rollup/rollup-linux-riscv64-gnu": "4.29.1",
"@rollup/rollup-linux-s390x-gnu": "4.29.1",
"@rollup/rollup-linux-x64-gnu": "4.29.1",
"@rollup/rollup-linux-x64-musl": "4.29.1",
"@rollup/rollup-win32-arm64-msvc": "4.29.1",
"@rollup/rollup-win32-ia32-msvc": "4.29.1",
"@rollup/rollup-win32-x64-msvc": "4.29.1",
"@rollup/rollup-android-arm-eabi": "4.34.2",
"@rollup/rollup-android-arm64": "4.34.2",
"@rollup/rollup-darwin-arm64": "4.34.2",
"@rollup/rollup-darwin-x64": "4.34.2",
"@rollup/rollup-freebsd-arm64": "4.34.2",
"@rollup/rollup-freebsd-x64": "4.34.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.2",
"@rollup/rollup-linux-arm-musleabihf": "4.34.2",
"@rollup/rollup-linux-arm64-gnu": "4.34.2",
"@rollup/rollup-linux-arm64-musl": "4.34.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.2",
"@rollup/rollup-linux-riscv64-gnu": "4.34.2",
"@rollup/rollup-linux-s390x-gnu": "4.34.2",
"@rollup/rollup-linux-x64-gnu": "4.34.2",
"@rollup/rollup-linux-x64-musl": "4.34.2",
"@rollup/rollup-win32-arm64-msvc": "4.34.2",
"@rollup/rollup-win32-ia32-msvc": "4.34.2",
"@rollup/rollup-win32-x64-msvc": "4.34.2",
"fsevents": "~2.3.2"
}
},
@ -3165,9 +3180,9 @@
}
},
"node_modules/sass": {
"version": "1.83.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz",
"integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==",
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz",
"integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3186,9 +3201,9 @@
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
@ -3326,9 +3341,9 @@
"license": "MIT"
},
"node_modules/unplugin": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz",
"integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==",
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
"integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -1,11 +1,9 @@
<template>
<div id="app">
<navbar-top />
<vue-progress-bar class="has-background-info" />
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
<modal-dialog-remote-pairing
:show="pairing_active"
@close="pairing_active = false"
@ -18,10 +16,9 @@
<navbar-bottom />
<div
v-show="show_burger_menu || show_player_menu"
class="fd-overlay-fullscreen"
class="overlay-fullscreen"
@click="show_burger_menu = show_player_menu = false"
/>
</div>
</template>
<script>

View File

@ -64,5 +64,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<a class="navbar-item" :href="href" @click.stop.prevent="open">
<a :href="href" @click.stop.prevent="open">
<slot />
</a>
</template>
@ -8,7 +8,7 @@
import { useUIStore } from '@/stores/ui'
export default {
name: 'NavbarItemLink',
name: 'ControlLink',
props: {
to: { required: true, type: Object }
},

View File

@ -0,0 +1,62 @@
<template>
<div class="media is-align-items-center mb-0">
<div class="media-left">
<a class="button is-small" @click="toggle">
<mdicon class="icon" :name="icon" />
</a>
</div>
<div class="media-content">
<div class="is-size-7 is-uppercase" v-text="$t('navigation.volume')" />
<control-slider
v-model:value="player.volume"
:cursor="cursor"
:max="100"
@change="changeVolume"
/>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import { mdiCancel } from '@mdi/js'
import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'ControlVolume',
components: { ControlSlider },
setup() {
return {
player: usePlayerStore()
}
},
data() {
return {
cursor: mdiCancel,
old_volume: 0
}
},
computed: {
icon() {
return this.player.volume > 0 ? 'volume-high' : 'volume-off'
}
},
watch: {
'player.volume'() {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
methods: {
changeVolume(value) {
webapi.player_volume(this.player.volume)
},
toggle() {
this.player.volume = this.player.volume > 0 ? 0 : this.old_volume
this.changeVolume()
}
}
}
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="media is-align-items-center mb-0">
<div class="media-left">
<a
class="button is-small"
:class="{ 'has-text-grey-light': !output.selected }"
@click="toggle"
>
<mdicon class="icon" :name="icon" :title="output.type" />
</a>
</div>
<div class="media-content">
<div
class="is-size-7 is-uppercase"
:class="{ 'has-text-grey-light': !output.selected }"
v-text="output.name"
/>
<control-slider
v-model:value="volume"
:disabled="!output.selected"
:max="100"
:cursor="cursor"
@change="changeVolume"
/>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import { mdiCancel } from '@mdi/js'
import webapi from '@/webapi'
export default {
name: 'ControlOutputVolume',
components: {
ControlSlider
},
props: { output: { required: true, type: Object } },
data() {
return {
cursor: mdiCancel,
volume: this.output.selected ? this.output.volume : 0
}
},
computed: {
icon() {
if (this.output.type.startsWith('AirPlay')) {
return 'cast-variant'
} else if (this.output.type === 'Chromecast') {
return 'cast'
} else if (this.output.type === 'fifo') {
return 'pipe'
}
return 'server'
}
},
watch: {
output() {
this.volume = this.output.volume
}
},
methods: {
changeVolume() {
webapi.player_output_volume(this.output.id, this.volume)
},
toggle() {
const values = {
selected: !this.output.selected
}
webapi.output_update(this.output.id, values)
}
}
}
</script>

View File

@ -1,8 +1,8 @@
<template>
<a v-if="visible" :disabled="disabled" @click="seek">
<mdicon
class="icon"
name="rewind-10"
:size="icon_size"
:title="$t('player.button.seek-backward')"
/>
</a>
@ -14,10 +14,9 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonSeekBack',
name: 'ControlPlayerBack',
props: {
icon_size: { default: 16, type: Number },
seek_ms: { required: true, type: Number }
offset: { required: true, type: Number }
},
setup() {
@ -52,7 +51,7 @@ export default {
methods: {
seek() {
if (!this.disabled) {
webapi.player_seek(this.seek_ms * -1)
webapi.player_seek(this.offset * -1)
}
}
}

View File

@ -1,9 +1,9 @@
<template>
<a :class="{ 'is-info': is_consume }" @click="toggle_consume_mode">
<a :class="{ 'is-info': is_consume }" @click="toggle">
<mdicon
class="icon"
name="fire"
:size="icon_size"
size="16"
:title="$t('player.button.consume')"
/>
</a>
@ -14,10 +14,7 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonConsume',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerConsume',
setup() {
return {
@ -32,11 +29,9 @@ export default {
},
methods: {
toggle_consume_mode() {
toggle() {
webapi.player_consume(!this.is_consume)
}
}
}
</script>
<style></style>

View File

@ -1,8 +1,8 @@
<template>
<a v-if="visible" :disabled="disabled" @click="seek">
<mdicon
class="icon"
name="fast-forward-30"
:size="icon_size"
:title="$t('player.button.seek-forward')"
/>
</a>
@ -14,10 +14,9 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonSeekForward',
name: 'ControlPlayerForward',
props: {
icon_size: { default: 16, type: Number },
seek_ms: { required: true, type: Number }
offset: { required: true, type: Number }
},
setup() {
@ -52,7 +51,7 @@ export default {
methods: {
seek() {
if (!this.disabled) {
webapi.player_seek(this.seek_ms)
webapi.player_seek(this.offset)
}
}
}

View File

@ -1,9 +1,9 @@
<template>
<a :class="{ 'is-info': is_active }" @click="toggle_lyrics">
<a :class="{ 'is-info': is_active }" @click="toggle">
<mdicon
class="icon"
:name="icon_name"
:size="icon_size"
:name="icon"
:size="16"
:title="$t('player.button.toggle-lyrics')"
/>
</a>
@ -13,10 +13,7 @@
import { useLyricsStore } from '@/stores/lyrics'
export default {
name: 'PlayerButtonLyrics',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerLyrics',
setup() {
return {
@ -25,7 +22,7 @@ export default {
},
computed: {
icon_name() {
icon() {
return this.is_active ? 'script-text-play' : 'script-text-outline'
},
is_active() {
@ -34,11 +31,9 @@ export default {
},
methods: {
toggle_lyrics() {
toggle() {
this.lyricsStore.pane = !this.lyricsStore.pane
}
}
}
</script>
<style></style>

View File

@ -1,8 +1,8 @@
<template>
<a :disabled="disabled" @click="play_next">
<mdicon
class="icon"
name="skip-forward"
:size="icon_size"
:title="$t('player.button.skip-forward')"
/>
</a>
@ -13,10 +13,7 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonNext',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerNext',
computed: {
disabled() {
@ -29,11 +26,8 @@ export default {
if (this.disabled) {
return
}
webapi.player_next()
}
}
}
</script>
<style></style>

View File

@ -1,10 +1,6 @@
<template>
<a :disabled="disabled" @click="toggle_play_pause">
<mdicon
:name="icon_name"
:size="icon_size"
:title="$t(`player.button.${icon_name}`)"
/>
<a :disabled="disabled" @click="toggle">
<mdicon class="icon" :name="icon" :title="$t(`player.button.${icon}`)" />
</a>
</template>
@ -15,9 +11,8 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonPlayPause',
name: 'ControlPlayerPlay',
props: {
icon_size: { default: 16, type: Number },
show_disabled_message: Boolean
},
@ -33,7 +28,7 @@ export default {
disabled() {
return this.queueStore?.count <= 0
},
icon_name() {
icon() {
if (!this.is_playing) {
return 'play'
} else if (this.is_pause_allowed) {
@ -51,7 +46,7 @@ export default {
},
methods: {
toggle_play_pause() {
toggle() {
if (this.disabled) {
if (this.show_disabled_message) {
this.notificationsStore.add({
@ -74,5 +69,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,8 +1,8 @@
<template>
<a :disabled="disabled" @click="play_previous">
<mdicon
class="icon"
name="skip-backward"
:size="icon_size"
:title="$t('player.button.skip-backward')"
/>
</a>
@ -13,10 +13,7 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonPrevious',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerPrevious',
setup() {
return {
@ -40,5 +37,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,10 +1,10 @@
<template>
<a :class="{ 'is-info': !is_repeat_off }" @click="toggle_repeat_mode">
<a :class="{ 'is-info': !is_repeat_off }" @click="toggle">
<mdicon
class="icon"
:name="icon_name"
:size="icon_size"
:title="$t(`player.button.${icon_name}`)"
:name="icon"
:size="16"
:title="$t(`player.button.${icon}`)"
/>
</a>
</template>
@ -14,19 +14,14 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonRepeat',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerRepeat',
setup() {
return {
playerStore: usePlayerStore()
}
},
computed: {
icon_name() {
icon() {
if (this.is_repeat_all) {
return 'repeat'
} else if (this.is_repeat_single) {
@ -46,7 +41,7 @@ export default {
},
methods: {
toggle_repeat_mode() {
toggle() {
if (this.is_repeat_all) {
webapi.player_repeat('single')
} else if (this.is_repeat_single) {
@ -58,5 +53,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,10 +1,10 @@
<template>
<a :class="{ 'is-info': is_shuffle }" @click="toggle_shuffle_mode">
<a :class="{ 'is-info': is_shuffle }" @click="toggle">
<mdicon
class="icon"
:name="icon_name"
:size="icon_size"
:title="$t(`player.button.${icon_name}`)"
:name="icon"
:size="16"
:title="$t(`player.button.${icon}`)"
/>
</a>
</template>
@ -14,20 +14,14 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonShuffle',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerShuffle',
setup() {
return {
playerStore: usePlayerStore()
}
},
computed: {
icon_name() {
icon() {
if (this.is_shuffle) {
return 'shuffle'
}
@ -37,13 +31,10 @@ export default {
return this.playerStore.shuffle
}
},
methods: {
toggle_shuffle_mode() {
toggle() {
webapi.player_shuffle(!this.is_shuffle)
}
}
}
</script>
<style></style>

View File

@ -0,0 +1,89 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label v-if="$slots.label" class="label has-text-weight-normal">
<slot name="label" />
</label>
<div class="control" :class="{ 'has-icons-right': isSuccess || isError }">
<slot name="input" :setting="setting" :update="update" />
<mdicon
v-if="isSuccess || isError"
class="icon is-right"
:name="isSuccess ? 'check' : 'close'"
size="16"
/>
</div>
<p v-if="$slots.help" class="help">
<slot name="help" />
</p>
</div>
</fieldset>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'ControlSetting',
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
timerDelay: 2000,
timerId: -1
}
},
computed: {
isError() {
return this.timerId === -2
},
isSuccess() {
return this.timerId >= 0
},
setting() {
return this.settingsStore.setting(this.category, this.name)
}
},
methods: {
update(event, sanitise) {
const value = sanitise?.(event.target)
if (value === this.setting.value) {
return
}
const setting = {
category: this.category,
name: this.name,
value
}
webapi
.settings_update(this.category, setting)
.then(() => {
window.clearTimeout(this.timerId)
this.settingsStore.update(setting)
})
.catch(() => {
this.timerId = -2
})
.finally(() => {
this.timerId = window.setTimeout(() => {
this.timerId = -1
}, this.timerDelay)
})
}
}
}
</script>

View File

@ -0,0 +1,46 @@
<template>
<control-setting
:category="category"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
>
<template #label>
<slot name="label" />
</template>
<template #input="{ setting, update }">
<input
class="input"
inputmode="numeric"
min="0"
:placeholder="placeholder"
:value="setting.value"
@input="update($event, sanitise)"
/>
</template>
<template #help>
<slot name="help" />
</template>
</control-setting>
</template>
<script>
import ControlSetting from '@/components/ControlSetting.vue'
export default {
name: 'ControlSettingIntegerField',
components: { ControlSetting },
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
methods: {
sanitise(target) {
const value = parseInt(target.value.replace(/\D+/gu, ''), 10) || 0
return (target.value = value)
}
}
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<control-setting :category="category" :disabled="disabled" :name="name">
<template #input="{ setting, update }">
<control-switch
:model-value="setting.value"
@update:model-value="
(value) => update({ target: { checked: value } }, sanitise)
"
>
<template #label>
<slot name="label" />
</template>
<template #help>
<slot name="help" />
</template>
</control-switch>
</template>
</control-setting>
</template>
<script>
import ControlSetting from '@/components/ControlSetting.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
export default {
name: 'ControlSettingSwitch',
components: { ControlSetting, ControlSwitch },
props: {
category: { required: true, type: String },
disabled: { default: false, type: Boolean },
name: { required: true, type: String }
},
methods: {
sanitise(target) {
return target.checked
}
}
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<control-setting
:category="category"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
>
<template #label>
<slot name="label" />
</template>
<template #input="{ setting, update }">
<input
class="input"
inputmode="text"
:placeholder="placeholder"
:value="setting.value"
@input="update($event, sanitise)"
/>
</template>
<template #help>
<slot name="help" />
</template>
</control-setting>
</template>
<script>
import ControlSetting from '@/components/ControlSetting.vue'
export default {
name: 'ControlSettingTextField',
components: { ControlSetting },
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
methods: {
sanitise(target) {
return target.value
}
}
}
</script>

View File

@ -32,3 +32,112 @@ export default {
}
}
</script>
<style lang="scss" scoped>
@use 'bulma/sass/utilities/mixins';
@mixin thumb {
-webkit-appearance: none;
width: var(--th);
height: var(--th);
box-sizing: border-box;
border-radius: 50%;
background: var(--bulma-light);
border: 1px solid var(--bulma-grey-lighter);
@media (prefers-color-scheme: dark) {
background: var(--bulma-grey-lighter);
border: 1px solid var(--bulma-grey-dark);
}
}
@mixin thumb-inactive {
box-sizing: border-box;
background-color: var(--bulma-light);
@media (prefers-color-scheme: dark) {
background-color: var(--bulma-grey-dark);
border: 1px solid var(--bulma-grey-darker);
}
}
@mixin track {
height: calc(var(--sh));
border-radius: calc(var(--sh) / 2);
background: linear-gradient(
90deg,
var(--bulma-dark) var(--sx),
var(--bulma-grey-lighter) var(--sx)
);
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
var(--bulma-grey-lighter) var(--sx),
var(--bulma-grey-dark) var(--sx)
);
}
}
@mixin track-inactive {
background: linear-gradient(
90deg,
var(--bulma-grey-light) var(--sx),
var(--bulma-light) var(--sx)
);
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
var(--bulma-grey-dark) var(--sx),
var(--bulma-black-ter) var(--sx)
);
}
}
input[type='range'].slider {
--sh: 0.25rem;
--th: calc(var(--sh) * 4);
background-color: transparent;
@include mixins.mobile {
--th: calc(var(--sh) * 5);
}
& {
--sx: calc(var(--th) / 2 + (var(--ratio) * (100% - var(--th))));
-webkit-appearance: none;
min-width: 250px;
height: calc(var(--sh) * 5);
width: 100% !important;
cursor: grab;
}
&:active {
cursor: grabbing;
}
&::-webkit-slider-thumb {
@include thumb;
& {
margin-top: calc((var(--th) - var(--sh)) / -2);
}
}
&::-moz-range-thumb {
@include thumb;
}
&::-webkit-slider-runnable-track {
@include track;
}
&::-moz-range-track {
@include track;
}
&.is-inactive {
cursor: var(--cursor, not-allowed);
&::-webkit-slider-thumb {
@include thumb-inactive;
}
&::-webkit-slider-runnable-track {
@include track-inactive;
}
&::-moz-range-thumb {
@include thumb-inactive;
}
&::-moz-range-track {
@include track-inactive;
}
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<div class="media is-align-items-center mb-0">
<div class="media-left">
<a
class="button is-small"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
>
<mdicon class="icon" name="broadcast" />
</a>
</div>
<div class="media-content is-align-items-center">
<div class="is-flex" :class="{ 'has-text-grey-light': !playing }">
<div class="is-size-7 is-uppercase" v-text="$t('navigation.stream')" />
<a href="stream.mp3" class="ml-2" target="_blank">
<mdicon class="icon is-small" name="open-in-new" />
</a>
</div>
<control-slider
v-model:value="volume"
:cursor="cursor"
:disabled="!playing"
:max="100"
@change="changeVolume"
/>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import audio from '@/lib/Audio'
import { mdiCancel } from '@mdi/js'
export default {
name: 'ControlStreamVolume',
components: { ControlSlider },
emits: ['change', 'mute'],
data() {
return {
cursor: mdiCancel,
loading: false,
playing: false,
volume: 10
}
},
mounted() {
this.setupAudio()
},
unmounted() {
this.closeAudio()
},
methods: {
changeVolume() {
audio.setVolume(this.volume / 100)
},
closeAudio() {
audio.stop()
this.playing = false
},
playChannel() {
if (this.playing) {
return
}
this.loading = true
audio.play('/stream.mp3')
audio.setVolume(this.volume / 100)
},
setupAudio() {
const a = audio.setup()
a.addEventListener('waiting', () => {
this.playing = false
this.loading = true
})
a.addEventListener('playing', () => {
this.playing = true
this.loading = false
})
a.addEventListener('ended', () => {
this.playing = false
this.loading = false
})
a.addEventListener('error', () => {
this.closeAudio()
this.notificationsStore.add({
text: this.$t('navigation.stream-error'),
type: 'danger'
})
this.playing = false
this.loading = false
})
},
togglePlay() {
if (this.loading) {
return
}
if (this.playing) {
this.closeAudio()
}
this.playChannel()
}
}
}
</script>

View File

@ -0,0 +1,78 @@
<template>
<div class="field">
<label class="toggle">
<div class="control is-flex is-align-content-center">
<input
:checked="modelValue"
type="checkbox"
class="toggle-checkbox"
@change="$emit('update:modelValue', !modelValue)"
/>
<div class="toggle-switch" />
<slot name="label" />
</div>
</label>
<div v-if="$slots.help" class="help">
<slot name="help" />
</div>
</div>
</template>
<script>
export default {
name: 'ControlSwitch',
props: {
modelValue: Boolean
},
emits: ['update:modelValue']
}
</script>
<style lang="scss" scoped>
.toggle {
cursor: pointer;
display: inline-block;
&-switch {
display: inline-block;
background: var(--bulma-grey-lighter);
border-radius: 1rem;
width: 2.5rem;
height: 1.25rem;
position: relative;
vertical-align: middle;
transition: background 0.25s;
margin-right: 0.5rem;
&:before {
content: '';
display: block;
background: var(--bulma-white);
border-radius: 50%;
width: 1rem;
height: 1rem;
position: absolute;
top: 0.125rem;
left: 0.125rem;
transition: left 0.25s;
}
}
&:hover &-switch:before {
background: var(--bulma-white);
}
&-checkbox {
position: absolute;
visibility: hidden;
&:checked + .toggle-switch {
background: var(--bulma-dark);
&:before {
left: 1.375rem;
}
}
}
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<section>
<nav class="buttons is-centered mb-4 fd-is-square">
<nav class="buttons is-centered mb-4">
<router-link
v-for="index in indices"
:key="index"
class="button is-small"
class="button is-small is-square"
:to="{ hash: `#index_${index}`, query: $route.query }"
>
{{ index }}
@ -20,4 +20,11 @@ export default {
}
</script>
<style></style>
<style scoped>
.is-square {
height: 1.75rem;
min-width: 1.75rem;
padding-left: 0.25rem;
padding-right: 0.25rem;
}
</style>

View File

@ -1,38 +1,36 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div
v-if="settingsStore.show_cover_artwork_in_album_lists"
class="media-left"
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<cover-artwork
v-if="settingsStore.show_cover_artwork_in_album_lists"
:url="item.item.artwork_url"
:artist="item.item.artist"
:album="item.item.name"
class="is-clickable fd-has-shadow fd-cover fd-cover-small-image"
class="media-left fd-has-shadow fd-cover fd-cover-small-image"
/>
</div>
<div class="media-content is-clickable is-clipped">
<div>
<h1 class="title is-6" v-text="item.item.name" />
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
<div
class="is-size-7 has-text-grey has-text-weight-bold"
v-text="item.item.artist"
/>
<h2
<div
v-if="item.item.date_released && item.item.media_kind === 'music'"
class="subtitle is-7 has-text-grey"
class="is-size-7 has-text-grey"
v-text="$filters.date(item.item.date_released)"
/>
</div>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
<mdicon class="icon has-text-dark" name="dots-vertical" size="16" />
@ -49,7 +47,7 @@
@remove-podcast="open_remove_podcast_dialog()"
@play-count-changed="play_count_changed()"
/>
<modal-dialog
<modal-dialog-action
:close_action="$t('page.podcast.cancel')"
:delete_action="$t('page.podcast.remove')"
:show="show_remove_podcast_modal"
@ -64,20 +62,20 @@
<b v-text="rss_playlist_to_remove.name" />)
</p>
</template>
</modal-dialog>
</modal-dialog-action>
</teleport>
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import ModalDialogAction from '@/components/ModalDialogAction.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'ListAlbums',
components: { CoverArtwork, ModalDialog, ModalDialogAlbum },
components: { CoverArtwork, ModalDialogAction, ModalDialogAlbum },
props: {
items: { required: true, type: Object },
media_kind: { default: '', type: String }
@ -148,5 +146,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,28 +1,29 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center" @click="open(item)">
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<div
v-if="settingsStore.show_cover_artwork_in_album_lists"
class="media-left is-clickable"
class="media-left"
>
<cover-artwork
:url="artwork_url(item)"
:artist="item.artist"
:album="item.name"
class="is-clickable fd-has-shadow fd-cover fd-cover-small-image"
class="fd-has-shadow fd-cover fd-cover-small-image"
/>
</div>
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.name" />
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.name" />
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.artists[0]?.name"
/>
<h2
class="subtitle is-7 has-text-grey"
v-text="
[item.album_type, $filters.date(item.release_date)].join(', ')
"
<div
class="is-size-7 has-text-grey"
v-text="$filters.date(item.release_date)"
/>
</div>
<div class="media-right">
@ -76,5 +77,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,17 +1,21 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@ -60,5 +64,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,8 +1,8 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center">
<div class="media-content is-clickable is-clipped" @click="open(item)">
<h1 class="title is-6" v-text="item.name" />
<div class="media is-align-items-center mb-0">
<div class="media-content is-clickable" @click="open(item)">
<p class="title is-6" v-text="item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@ -45,5 +45,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,17 +1,21 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@ -62,5 +66,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,9 +1,11 @@
<template>
<div v-if="$route.query.directory" class="media is-align-items-center">
<figure class="media-left is-clickable" @click="open_parent">
<mdicon class="icon" name="chevron-left" size="16" />
</figure>
<div class="media-content is-clipped">
<div v-if="$route.query.directory" class="media is-align-items-center mb-0">
<mdicon
class="icon media-left is-clickable"
name="chevron-left"
@click="open_parent"
/>
<div class="media-content">
<nav class="breadcrumb">
<ul>
<li v-for="directory in directories" :key="directory.index">
@ -19,12 +21,13 @@
</div>
</div>
<template v-for="item in items" :key="item.path">
<div class="media is-align-items-center" @click="open(item)">
<figure class="media-left is-clickable">
<mdicon class="icon" name="folder" size="16" />
</figure>
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.name" />
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<mdicon class="media-left icon" name="folder" />
<div class="media-content">
<p class="title is-6" v-text="item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@ -90,5 +93,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,17 +1,21 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@ -63,5 +67,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,26 +1,27 @@
<template>
<div
v-if="is_next || !show_only_next_items"
class="media is-align-items-center"
class="media is-align-items-center is-clickable mb-0"
@click="play"
>
<div v-if="edit_mode" class="media-left">
<mdicon
class="icon has-text-grey fd-is-movable handle"
class="icon has-text-grey is-movable"
name="drag-horizontal"
size="16"
size="18"
/>
</div>
<div class="media-content is-clickable is-clipped" @click="play">
<h1
class="title is-6"
<div class="media-content">
<div
class="is-size-6 has-text-weight-bold"
:class="{
'has-text-primary': item.id === player.item_id,
'has-text-grey-light': !is_next
}"
v-text="item.title"
/>
<h2
class="subtitle is-7 has-text-weight-bold"
<div
class="is-size-7 has-text-weight-bold"
:class="{
'has-text-primary': item.id === player.item_id,
'has-text-grey-light': !is_next,
@ -28,8 +29,8 @@
}"
v-text="item.artist"
/>
<h2
class="subtitle is-7"
<div
class="is-size-7"
:class="{
'has-text-primary': item.id === player.item_id,
'has-text-grey-light': !is_next,
@ -81,4 +82,8 @@ export default {
}
</script>
<style></style>
<style scoped>
.is-movable {
cursor: move;
}
</style>

View File

@ -1,11 +1,12 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div class="media is-align-items-center" @click="open(item.item)">
<figure class="media-left is-clickable">
<mdicon class="icon" :name="icon_name(item.item)" size="16" />
</figure>
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<mdicon class="media-left icon" :name="icon(item.item)" />
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@ -39,7 +40,7 @@ export default {
},
methods: {
icon_name(item) {
icon(item) {
if (item.type === 'folder') {
return 'folder'
} else if (item.type === 'rss') {
@ -61,5 +62,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,9 +1,15 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center">
<div class="media-content is-clickable is-clipped" @click="open(item)">
<h1 class="title is-6" v-text="item.name" />
<h2 class="subtitle is-7" v-text="item.owner.display_name" />
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.name" />
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.owner.display_name"
/>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@ -46,5 +52,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,35 +1,33 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div
v-else
class="media is-align-items-center"
class="media is-align-items-center is-clickable mb-0"
:class="{ 'with-progress': show_progress }"
@click="play(item.item)"
>
<figure v-if="show_icon" class="media-left is-clickable">
<mdicon class="icon" name="file-outline" size="16" />
</figure>
<div class="media-content is-clickable is-clipped">
<h1
class="title is-6"
<mdicon v-if="show_icon" class="media-left icon" name="file-outline" />
<div class="media-content">
<div
class="is-size-6 has-text-weight-bold"
:class="{
'has-text-grey':
item.item.media_kind === 'podcast' && item.item.play_count > 0
}"
v-text="item.item.title"
/>
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.item.artist"
/>
<h2 class="subtitle is-7 has-text-grey" v-text="item.item.album" />
<div class="is-size-7 has-text-grey" v-text="item.item.album" />
<progress
v-if="show_progress && item.item.seek_ms > 0"
class="progress is-info"
@ -103,4 +101,8 @@ export default {
.progress {
height: 0.25rem;
}
.media.with-progress {
margin-top: 0.375rem;
}
</style>

View File

@ -1,29 +1,29 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center">
<div class="media is-align-items-center mb-0">
<div
class="media-content is-clipped"
class="media-content"
:class="{
'is-clickable': item.is_playable,
'fd-is-not-allowed': !item.is_playable
'is-not-allowed': !item.is_playable
}"
@click="play(item)"
>
<h1
class="title is-6"
<div
class="is-size-6 has-text-weight-bold"
:class="{ 'has-text-grey-light': !item.is_playable }"
v-text="item.name"
/>
<h2
class="subtitle is-7 has-text-weight-bold"
<div
class="is-size-7 has-text-weight-bold"
:class="{
'has-text-grey': item.is_playable,
'has-text-grey-light': !item.is_playable
}"
v-text="item.artists[0].name"
/>
<h2 class="subtitle is-7 has-text-grey" v-text="item.album.name" />
<h2 v-if="!item.is_playable" class="subtitle is-7">
<div class="is-size-7 has-text-grey" v-text="item.album.name" />
<div v-if="!item.is_playable" class="is-size-7 has-text-grey">
(<span v-text="$t('list.spotify.not-playable-track')" />
<span
v-if="item.restrictions?.reason"
@ -33,7 +33,7 @@
})
"
/>)
</h2>
</div>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@ -83,4 +83,8 @@ export default {
}
</script>
<style></style>
<style scoped>
.is-not-allowed {
cursor: not-allowed;
}
</style>

View File

@ -223,4 +223,14 @@ export default {
.lyrics div:last-child {
padding-bottom: calc(25vh - 3rem);
}
/* Lyrics animation */
@keyframes pop-color {
0% {
color: var(--bulma-black);
}
100% {
color: var(--bulma-success);
}
}
</style>

View File

@ -5,38 +5,13 @@
<div class="modal-content">
<div class="card">
<div class="card-content">
<p v-if="title" class="title is-4" v-text="title" />
<slot name="modal-content" />
<slot name="content" />
</div>
<footer class="card-footer is-clipped">
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="close_action" />
</a>
<a
v-if="delete_action"
class="card-footer-item has-background-danger has-text-white has-text-weight-bold"
@click="$emit('delete')"
>
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="delete_action" />
</a>
<a
v-if="ok_action"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="$emit('ok')"
>
<mdicon class="icon" name="check" size="16" />
<span class="is-size-7" v-text="ok_action" />
</a>
<slot name="footer" />
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
@ -45,14 +20,35 @@
export default {
name: 'ModalDialog',
props: {
close_action: { default: '', type: String },
delete_action: { default: '', type: String },
ok_action: { default: '', type: String },
show: Boolean,
title: { required: true, type: String }
show: Boolean
},
emits: ['delete', 'close', 'ok']
emits: ['close'],
watch: {
show(value) {
const { classList } = document.querySelector('html')
if (value) {
classList.add('is-clipped')
} else {
classList.remove('is-clipped')
}
}
}
}
</script>
<style></style>
<style scoped>
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p v-if="title" class="title is-4" v-text="title" />
<slot name="modal-content" />
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="close_action" />
</a>
<a
v-if="delete_action"
class="card-footer-item has-background-danger"
@click="$emit('delete')"
>
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="delete_action" />
</a>
<a v-if="ok_action" class="card-footer-item" @click="$emit('ok')">
<mdicon class="icon" name="check" size="16" />
<span class="is-size-7" v-text="ok_action" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
export default {
name: 'ModalDialogAction',
components: { ModalDialog },
props: {
close_action: { default: '', type: String },
delete_action: { default: '', type: String },
ok_action: { default: '', type: String },
show: Boolean,
title: { required: true, type: String }
},
emits: ['delete', 'close', 'ok'],
watch: {
show() {
const { classList } = document.querySelector('html')
if (this.show) {
classList.add('is-clipped')
} else {
classList.remove('is-clipped')
}
}
}
}
</script>

View File

@ -1,17 +1,13 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<form class="card" @submit.prevent="add_stream">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="$t('dialog.add.rss.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="url_field"
v-model="url"
class="input is-shadowless"
class="input"
type="url"
pattern="http[s]?://.+"
required
@ -23,46 +19,37 @@
</p>
<p class="help" v-text="$t('dialog.add.rss.help')" />
</div>
</div>
<footer v-if="loading" class="card-footer">
</template>
<template v-if="loading" #footer>
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.add.rss.processing')"
/>
<span class="is-size-7" v-text="$t('dialog.add.rss.processing')" />
</a>
</footer>
<footer v-else class="card-footer is-clipped">
</template>
<template v-else #footer>
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
class="card-footer-item"
@click="add_stream"
>
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.add')" />
</a>
</footer>
</form>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAddRss',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close', 'podcast-added'],
@ -107,5 +94,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,17 +1,14 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<form class="card" @submit.prevent="play">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<form @submit.prevent="play">
<p class="title is-4" v-text="$t('dialog.add.stream.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="url_field"
v-model="url"
class="input is-shadowless"
class="input"
type="url"
pattern="http[s]?://.+"
required
@ -22,17 +19,15 @@
<mdicon class="icon is-left" name="web" size="16" />
</p>
</div>
</div>
<footer v-if="loading" class="card-footer">
</form>
</template>
<template v-if="loading" #footer>
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.add.stream.loading')"
/>
<span class="is-size-7" v-text="$t('dialog.add.stream.loading')" />
</a>
</footer>
<footer v-else class="card-footer is-clipped">
</template>
<template v-else #footer>
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.cancel')" />
@ -47,29 +42,23 @@
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
class="card-footer-item has-text-dark"
@click="play"
>
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.play')" />
</a>
</footer>
</form>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAddUrlStream',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close'],
@ -125,5 +114,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,19 +1,15 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<cover-artwork
:url="item.artwork_url"
:artist="item.artist"
:album="item.name"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-5"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-3"
/>
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div v-if="media_kind_resolved === 'podcast'" class="buttons">
<a
class="button is-small"
@ -27,59 +23,61 @@
v-text="$t('dialog.album.remove-podcast')"
/>
</div>
<div class="content is-small">
<p v-if="item.artist">
<span class="heading" v-text="$t('dialog.album.artist')" />
<a
class="title is-6 has-text-link"
@click="open_artist"
v-text="item.artist"
<div v-if="item.artist" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.artist')"
/>
</p>
<p v-if="item.date_released">
<span
class="heading"
<div class="title is-6">
<a @click="open_artist" v-text="item.artist" />
</div>
</div>
<div v-if="item.date_released" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.date_released)"
<div class="title is-6" v-text="$filters.date(item.date_released)" />
</div>
<div v-else-if="item.year" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.album.year')" />
<div class="title is-6" v-text="item.year" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.tracks')"
/>
</p>
<p v-else-if="item.year">
<span class="heading" v-text="$t('dialog.album.year')" />
<span class="title is-6" v-text="item.year" />
</p>
<p>
<span class="heading" v-text="$t('dialog.album.tracks')" />
<span class="title is-6" v-text="item.track_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.album.duration')" />
<span
<div class="title is-6" v-text="item.track_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.album.type')" />
<span
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.album.type')" />
<div
class="title is-6"
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.album.added-on')" />
<span
class="title is-6"
v-text="$filters.datetime(item.time_added)"
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.added-on')"
/>
</p>
<div class="title is-6" v-text="$filters.datetime(item.time_added)" />
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.add')" />
@ -92,25 +90,18 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAlbum',
components: { CoverArtwork },
components: { ModalDialog, CoverArtwork },
props: {
item: { required: true, type: Object },
media_kind: { default: '', type: String },
@ -184,5 +175,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,90 +1,66 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<cover-artwork
:url="artwork_url(item)"
:artist="item.artist"
:album="item.name"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-5"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-3"
@load="artwork_loaded"
@error="artwork_error"
/>
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span
class="heading"
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.album.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_artist"
v-text="item.artists[0].name"
/>
</p>
<p>
<span
class="heading"
<div class="title is-6">
<a @click="open_artist" v-text="item.artists[0].name" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.album.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.release_date)"
/>
</p>
<p>
<span
class="heading"
<div class="title is-6" v-text="$filters.date(item.release_date)" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.album.type')"
/>
<span class="title is-6" v-text="item.album_type" />
</p>
<div class="title is-6" v-text="item.album_type" />
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.album.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.album.add-next')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.album.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.album.play')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.album.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAlbumSpotify',
components: { CoverArtwork },
components: { ModalDialog, CoverArtwork },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -133,5 +109,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,39 +1,36 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span class="heading" v-text="$t('dialog.artist.albums')" />
<span class="title is-6" v-text="item.album_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.artist.tracks')" />
<span class="title is-6" v-text="item.track_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.artist.type')" />
<span
class="title is-6"
v-text="$t(`data.kind.${item.data_kind}`)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.artist.added-on')" />
<span
class="title is-6"
v-text="$filters.datetime(item.time_added)"
/>
</p>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.artist.albums')"
/>
<div class="title is-6" v-text="item.album_count" />
</div>
<footer class="card-footer">
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.artist.tracks')"
/>
<div class="title is-6" v-text="item.track_count" />
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.artist.type')" />
<div class="title is-6" v-text="$t(`data.kind.${item.data_kind}`)" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.artist.added-on')"
/>
<div class="title is-6" v-text="$filters.datetime(item.time_added)" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.add')" />
@ -46,23 +43,17 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogArtist',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -89,5 +80,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,72 +1,51 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span
class="heading"
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.artist.popularity')"
/>
<span
<div
class="title is-6"
v-text="[item.popularity, item.followers.total].join(' / ')"
/>
</p>
<p>
<span
class="heading"
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.artist.genres')"
/>
<span class="title is-6" v-text="item.genres.join(', ')" />
</p>
<div class="title is-6" v-text="item.genres.join(', ')" />
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.artist.add')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.artist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.artist.add-next')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.artist.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.artist.play')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.artist.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogArtistSpotify',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -93,5 +72,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,42 +1,39 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a
class="has-text-link"
@click="open_albums"
v-text="item.name"
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open_albums" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.composer.albums')"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.composer.albums')" />
<a
class="has-text-link is-6"
@click="open_albums"
v-text="item.album_count"
<div class="title is-6">
<a @click="open_albums" v-text="item.album_count" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.composer.tracks')"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.composer.tracks')" />
<a
class="has-text-link is-6"
@click="open_tracks"
v-text="item.track_count"
<div class="title is-6">
<a @click="open_tracks" v-text="item.track_count" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.composer.duration')"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.composer.duration')" />
<span
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.add')" />
@ -49,23 +46,17 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogComposer',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -105,5 +96,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,45 +1,32 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="item" />
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.directory.add-next')"
/>
<span class="is-size-7" v-text="$t('dialog.directory.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogDirectory',
components: { ModalDialog },
props: { item: { required: true, type: String }, show: Boolean },
emits: ['close'],
@ -66,5 +53,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,32 +1,35 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span class="heading" v-text="$t('dialog.genre.albums')" />
<span class="title is-6" v-text="item.album_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.genre.tracks')" />
<span class="title is-6" v-text="item.track_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.genre.duration')" />
<span
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.genre.albums')"
/>
<div class="title is-6" v-text="item.album_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.genre.tracks')"
/>
<div class="title is-6" v-text="item.track_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.genre.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.add')" />
@ -39,23 +42,17 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogGenre',
components: { ModalDialog },
props: {
item: { required: true, type: Object },
media_kind: { required: true, type: String },
@ -92,5 +89,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,32 +1,32 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span class="heading" v-text="$t('dialog.playlist.path')" />
<span class="title is-6" v-text="item.path" />
</p>
<p>
<span class="heading" v-text="$t('dialog.playlist.type')" />
<span
class="title is-6"
v-text="$t(`playlist.type.${item.type}`)"
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.playlist.path')"
/>
</p>
<p v-if="!item.folder">
<span class="heading" v-text="$t('dialog.playlist.tracks')" />
<span class="title is-6" v-text="item.item_count" />
</p>
<div class="title is-6" v-text="item.path" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.playlist.type')"
/>
<div class="title is-6" v-text="$t(`playlist.type.${item.type}`)" />
</div>
<footer v-if="!item.folder" class="card-footer">
<div v-if="!item.folder" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.playlist.tracks')"
/>
<div class="title is-6" v-text="item.item_count" />
</div>
</template>
<template v-if="!item.folder" #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.add')" />
@ -39,23 +39,17 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylist',
components: { ModalDialog },
props: {
item: { required: true, type: Object },
show: Boolean,
@ -86,5 +80,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,17 +1,14 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<form class="card" @submit.prevent="save">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<form @submit.prevent="save">
<p class="title is-4" v-text="$t('dialog.playlist.save.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="playlist_name_field"
v-model="playlist_name"
class="input is-shadowless"
class="input"
type="text"
pattern=".+"
required
@ -22,52 +19,38 @@
<mdicon class="icon is-left" name="file-music" size="16" />
</p>
</div>
</div>
<footer v-if="loading" class="card-footer">
</form>
</template>
<template v-if="loading" #footer>
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.playlist.save.saving')"
/>
<span class="is-size-7" v-text="$t('dialog.playlist.save.saving')" />
</a>
</footer>
<footer v-else class="card-footer is-clipped">
</template>
<template v-else #footer>
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.playlist.save.cancel')"
/>
<span class="is-size-7" v-text="$t('dialog.playlist.save.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
class="card-footer-item has-text-weight-bold"
@click="save"
>
<mdicon class="icon" name="content-save" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.playlist.save.save')"
/>
<span class="is-size-7" v-text="$t('dialog.playlist.save.save')" />
</a>
</footer>
</form>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylistSave',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close'],
@ -111,5 +94,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,44 +1,35 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span
class="heading"
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.playlist.owner')"
/>
<span class="title is-6" v-text="item.owner.display_name" />
</p>
<p>
<span
class="heading"
<div class="title is-6" v-text="item.owner.display_name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.playlist.tracks')"
/>
<span class="title is-6" v-text="item.tracks.total" />
</p>
<p>
<span
class="heading"
<div class="title is-6" v-text="item.tracks.total" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.playlist.path')"
/>
<span class="title is-6" v-text="item.uri" />
</p>
<div class="title is-6" v-text="item.uri" />
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.playlist.add')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.playlist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
@ -49,28 +40,19 @@
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.playlist.play')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.playlist.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylistSpotify',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -97,5 +79,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,91 +1,95 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4" v-text="item.title" />
<p class="subtitle" v-text="item.artist" />
<div class="content is-small">
<p v-if="item.album">
<span class="heading" v-text="$t('dialog.queue-item.album')" />
<a
class="title is-6 has-text-link"
@click="open_album"
v-text="item.album"
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4" v-text="item.title" />
<div class="subtitle" v-text="item.artist" />
<div v-if="item.album" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.album')"
/>
</p>
<p v-if="item.album_artist">
<span
class="heading"
<div class="title is-6">
<a @click="open_album" v-text="item.album" />
</div>
</div>
<div v-if="item.album_artist" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_album_artist"
v-text="item.album_artist"
/>
</p>
<p v-if="item.composer">
<span
class="heading"
<div class="title is-6">
<a @click="open_album_artist" v-text="item.album_artist" />
</div>
</div>
<div v-if="item.composer" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.composer')"
/>
<span class="title is-6" v-text="item.composer" />
</p>
<p v-if="item.year">
<span class="heading" v-text="$t('dialog.queue-item.year')" />
<span class="title is-6" v-text="item.year" />
</p>
<p v-if="item.genre">
<span class="heading" v-text="$t('dialog.queue-item.genre')" />
<a
class="title is-6 has-text-link"
@click="open_genre"
v-text="item.genre"
<div class="title is-6" v-text="item.composer" />
</div>
<div v-if="item.year" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.year')"
/>
</p>
<p v-if="item.disc_number">
<span
class="heading"
<div class="title is-6" v-text="item.year" />
</div>
<div v-if="item.genre" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.genre')"
/>
<div class="title is-6">
<a @click="open_genre" v-text="item.genre" />
</div>
</div>
<div v-if="item.disc_number" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.position')"
/>
<span
<div
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</p>
<p v-if="item.length_ms">
<span
class="heading"
</div>
<div v-if="item.length_ms" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.duration')"
/>
<span
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.queue-item.path')" />
<span class="title is-6" v-text="item.path" />
</p>
<p>
<span class="heading" v-text="$t('dialog.queue-item.type')" />
<span class="title is-6">
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.path')"
/>
<div class="title is-6" v-text="item.path" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.type')"
/>
<div class="title is-6">
<span
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</span>
</p>
<p v-if="item.samplerate">
<span
class="heading"
</div>
</div>
<div v-if="item.samplerate" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.quality')"
/>
<span class="title is-6">
<div class="title is-6">
<span v-text="item.type" />
<span
v-if="item.samplerate"
@ -105,15 +109,12 @@
/>
<span
v-if="item.bitrate"
v-text="
$t('dialog.queue-item.bitrate', { rate: item.bitrate })
"
v-text="$t('dialog.queue-item.bitrate', { rate: item.bitrate })"
/>
</span>
</p>
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="remove">
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="$t('dialog.queue-item.remove')" />
@ -122,25 +123,19 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.queue-item.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi'
export default {
name: 'ModalDialogQueueItem',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -233,5 +228,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,10 +1,6 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="$t('dialog.remote-pairing.title')" />
<form @submit.prevent="kickoff_pairing">
<label class="label" v-text="pairing.remote" />
@ -21,43 +17,28 @@
</div>
</div>
</form>
</div>
<footer class="card-footer is-clipped">
</template>
<template #footer>
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.remote-pairing.cancel')"
/>
<span class="is-size-7" v-text="$t('dialog.remote-pairing.cancel')" />
</a>
<a
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="kickoff_pairing"
>
<a class="card-footer-item" @click="kickoff_pairing">
<mdicon class="icon" name="cellphone" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.remote-pairing.pair')"
/>
<span class="is-size-7" v-text="$t('dialog.remote-pairing.pair')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import { useRemotesStore } from '@/stores/remotes'
import webapi from '@/webapi'
export default {
name: 'ModalDialogRemotePairing',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close'],
@ -98,5 +79,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,10 +1,6 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="item.title" />
<p class="subtitle" v-text="item.artist" />
<div v-if="item.media_kind === 'podcast'" class="buttons">
@ -21,83 +17,87 @@
v-text="$t('dialog.track.mark-as-played')"
/>
</div>
<div class="content is-small">
<p v-if="item.album">
<span class="heading" v-text="$t('dialog.track.album')" />
<a
class="title is-6 has-text-link"
@click="open_album"
v-text="item.album"
/>
</p>
<p v-if="item.album_artist && item.media_kind !== 'audiobook'">
<span
class="heading"
<div v-if="item.album" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.album')" />
<div class="title is-6">
<a @click="open_album" v-text="item.album" />
</div>
</div>
<div
v-if="item.album_artist && item.media_kind !== 'audiobook'"
class="mb-3"
>
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_album_artist"
v-text="item.album_artist"
<div class="title is-6">
<a @click="open_album_artist" v-text="item.album_artist" />
</div>
</div>
<div v-if="item.composer" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.composer')"
/>
</p>
<p v-if="item.composer">
<span class="heading" v-text="$t('dialog.track.composer')" />
<span class="title is-6" v-text="item.composer" />
</p>
<p v-if="item.date_released">
<span
class="heading"
<div class="title is-6" v-text="item.composer" />
</div>
<div v-if="item.date_released" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.date_released)"
<div class="title is-6" v-text="$filters.date(item.date_released)" />
</div>
<div v-else-if="item.year" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.year')" />
<div class="title is-6" v-text="item.year" />
</div>
<div v-if="item.genre" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.genre')" />
<div class="title is-6">
<a @click="open_genre" v-text="item.genre" />
</div>
</div>
<div v-if="item.disc_number" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.position')"
/>
</p>
<p v-else-if="item.year">
<span class="heading" v-text="$t('dialog.track.year')" />
<span class="title is-6" v-text="item.year" />
</p>
<p v-if="item.genre">
<span class="heading" v-text="$t('dialog.track.genre')" />
<a
class="title is-6 has-text-link"
@click="open_genre"
v-text="item.genre"
/>
</p>
<p v-if="item.disc_number">
<span class="heading" v-text="$t('dialog.track.position')" />
<span
<div
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</p>
<p v-if="item.length_ms">
<span class="heading" v-text="$t('dialog.track.duration')" />
<span
</div>
<div v-if="item.length_ms" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.track.path')" />
<span class="title is-6" v-text="item.path" />
</p>
<p>
<span class="heading" v-text="$t('dialog.track.type')" />
<span class="title is-6">
<span
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.path')" />
<div class="title is-6" v-text="item.path" />
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.type')" />
<div
class="title is-6"
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</span>
</p>
<p v-if="item.samplerate">
<span class="heading" v-text="$t('dialog.track.quality')" />
<span class="title is-6">
</div>
<div v-if="item.samplerate" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.quality')"
/>
<div class="title is-6">
<span v-text="item.type" />
<span
v-if="item.samplerate"
@ -119,18 +119,21 @@
v-if="item.bitrate"
v-text="$t('dialog.track.bitrate', { rate: item.bitrate })"
/>
</span>
</p>
<p>
<span class="heading" v-text="$t('dialog.track.added-on')" />
<span
class="title is-6"
v-text="$filters.datetime(item.time_added)"
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.added-on')"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.track.rating')" />
<span
<div class="title is-6" v-text="$filters.datetime(item.time_added)" />
</div>
<div>
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.rating')"
/>
<div
class="title is-6"
v-text="
$t('dialog.track.rating-value', {
@ -138,14 +141,16 @@
})
"
/>
</p>
<p v-if="item.comment">
<span class="heading" v-text="$t('dialog.track.comment')" />
<span class="title is-6" v-text="item.comment" />
</p>
</div>
<div v-if="item.comment" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.comment')"
/>
<div class="title is-6" v-text="item.comment" />
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.add')" />
@ -158,25 +163,19 @@
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi'
export default {
name: 'ModalDialogTrack',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close', 'play-count-changed'],
@ -296,5 +295,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,110 +1,88 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="item.name" />
<p class="subtitle" v-text="item.artists[0].name" />
<div class="content is-small">
<p>
<span
class="heading"
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.album')"
/>
<a
class="title is-6 has-text-link"
@click="open_album"
v-text="item.album.name"
/>
</p>
<p>
<span
class="heading"
<div class="title is-6">
<a @click="open_album" v-text="item.album.name" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_artist"
v-text="item.artists[0].name"
/>
</p>
<p>
<span
class="heading"
<div class="title is-6">
<a @click="open_artist" v-text="item.artists[0].name" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.release-date')"
/>
<span
<div
class="title is-6"
v-text="$filters.date(item.album.release_date)"
/>
</p>
<p>
<span
class="heading"
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.position')"
/>
<span
<div
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</p>
<p>
<span
class="heading"
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.duration')"
/>
<span
<div
class="title is-6"
v-text="$filters.durationInHours(item.duration_ms)"
/>
</p>
<p>
<span
class="heading"
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.path')"
/>
<span class="title is-6" v-text="item.uri" />
</p>
<div class="title is-6" v-text="item.uri" />
</div>
</div>
<footer class="card-footer">
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.track.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.track.add-next')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.track.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.track.play')"
/>
<span class="is-size-7" v-text="$t('dialog.spotify.track.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogTrackSpotify',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@ -138,5 +116,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,19 +1,19 @@
<template>
<modal-dialog
<modal-dialog-action
:show="show"
:title="$t('dialog.update.title')"
:ok_action="library.updating ? '' : $t('dialog.update.rescan')"
:ok_action="libraryStore.updating ? '' : $t('dialog.update.rescan')"
:close_action="$t('dialog.update.cancel')"
@ok="update_library"
@close="close()"
>
<template #modal-content>
<div v-if="!library.updating">
<p class="mb-3" v-text="$t('dialog.update.info')" />
<div v-if="!libraryStore.updating">
<div v-if="spotify_enabled || rss.tracks > 0" class="field">
<label class="label" v-text="$t('dialog.update.info')" />
<div class="control">
<div class="select is-small">
<select v-model="update_dialog_scan_kind">
<select v-model="libraryStore.update_dialog_scan_kind">
<option value="" v-text="$t('dialog.update.all')" />
<option value="files" v-text="$t('dialog.update.local')" />
<option
@ -30,32 +30,29 @@
</div>
</div>
</div>
<div class="field">
<input
id="rescan"
v-model="rescan_metadata"
type="checkbox"
class="switch is-rounded is-small"
/>
<label for="rescan" v-text="$t('dialog.update.rescan-metadata')" />
</div>
<control-switch v-model="rescan_metadata">
<template #label>
<span v-text="$t('dialog.update.rescan-metadata')" />
</template>
</control-switch>
</div>
<div v-else>
<p class="mb-3" v-text="$t('dialog.update.progress')" />
</div>
</template>
</modal-dialog>
</modal-dialog-action>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import ModalDialogAction from '@/components/ModalDialogAction.vue'
import { useLibraryStore } from '@/stores/library'
import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi'
export default {
name: 'ModalDialogUpdate',
components: { ModalDialog },
components: { ControlSwitch, ModalDialogAction },
props: { show: Boolean },
emits: ['close'],
@ -73,42 +70,26 @@ export default {
},
computed: {
library() {
return this.libraryStore.$state
},
rss() {
return this.libraryStore.rss
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
},
update_dialog_scan_kind: {
get() {
return this.library.update_dialog_scan_kind
},
set(value) {
this.library.update_dialog_scan_kind = value
}
}
},
methods: {
close() {
this.update_dialog_scan_kind = ''
this.libraryStore.update_dialog_scan_kind = ''
this.$emit('close')
},
update_library() {
if (this.rescan_metadata) {
webapi.library_rescan(this.update_dialog_scan_kind)
webapi.library_rescan(this.libraryStore.update_dialog_scan_kind)
} else {
webapi.library_update(this.update_dialog_scan_kind)
webapi.library_update(this.libraryStore.update_dialog_scan_kind)
}
}
}
}
</script>
<style></style>

View File

@ -1,118 +1,26 @@
<template>
<nav
class="navbar is-block is-white is-fixed-bottom fd-bottom-navbar"
:class="{
'is-transparent': is_now_playing_page,
'is-dark': !is_now_playing_page
}"
role="navigation"
aria-label="player controls"
class="navbar is-fixed-bottom"
:class="{ 'is-dark': !is_now_playing_page }"
>
<!-- Player menu for desktop -->
<div
class="navbar-item has-dropdown has-dropdown-up is-hidden-touch"
:class="{ 'is-active': show_player_menu }"
>
<div class="navbar-dropdown is-right fd-width-auto">
<div class="navbar-item">
<!-- Outputs: master volume -->
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a class="button is-white is-small" @click="toggle_mute_volume">
<mdicon
class="icon"
:name="player.volume > 0 ? 'volume-high' : 'volume-off'"
size="18"
/>
</a>
</div>
<div class="level-item">
<div>
<p class="heading" v-text="$t('navigation.volume')" />
<control-slider
v-model:value="player.volume"
:max="100"
@change="change_volume"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Outputs: master volume -->
<hr class="my-3" />
<navbar-item-output
v-for="output in outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume -->
<hr class="my-3" />
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a
class="button is-white is-small"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
>
<mdicon class="icon" name="broadcast" size="18" />
</a>
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<div
class="is-flex is-align-content-center"
:class="{ 'has-text-grey-light': !playing }"
>
<p class="heading" v-text="$t('navigation.stream')" />
<a href="stream.mp3" class="heading ml-2" target="_blank">
<mdicon
class="icon is-small"
name="open-in-new"
size="16"
/>
</a>
</div>
<control-slider
v-model:value="stream_volume"
:disabled="!playing"
:max="100"
:cursor="cursor"
@change="change_stream_volume"
/>
</div>
</div>
</div>
</div>
</div>
<hr class="my-3" />
<div class="navbar-item is-justify-content-center">
<div class="buttons has-addons">
<player-button-repeat class="button" />
<player-button-shuffle class="button" />
<player-button-consume class="button" />
<player-button-lyrics class="button" />
</div>
</div>
</div>
</div>
<div class="navbar-brand is-flex-grow-1">
<navbar-item-link :to="{ name: 'queue' }" class="mr-auto">
<mdicon class="icon" name="playlist-play" size="24" />
</navbar-item-link>
<navbar-item-link
v-if="!is_now_playing_page"
<control-link class="navbar-item" :to="{ name: 'queue' }">
<mdicon class="icon" name="playlist-play" />
</control-link>
<template v-if="is_now_playing_page">
<control-player-previous class="navbar-item ml-auto" />
<control-player-back class="navbar-item" :offset="10000" />
<control-player-play class="navbar-item" show_disabled_message />
<control-player-forward class="navbar-item" :offset="30000" />
<control-player-next class="navbar-item mr-auto" />
</template>
<template v-else>
<control-link
:to="{ name: 'now-playing' }"
exact
class="is-expanded is-clipped is-size-7"
class="navbar-item is-justify-content-flex-start is-expanded is-clipped is-size-7"
>
<div class="fd-is-text-clipped">
<div class="is-text-clipped">
<strong v-text="current.title" />
<br />
<span v-text="current.artist" />
@ -121,130 +29,40 @@
v-text="$t('navigation.now-playing', { album: current.album })"
/>
</div>
</navbar-item-link>
<player-button-previous
v-if="is_now_playing_page"
class="navbar-item px-2"
:icon_size="24"
/>
<player-button-seek-back
v-if="is_now_playing_page"
:seek_ms="10000"
class="navbar-item px-2"
:icon_size="24"
/>
<player-button-play-pause
class="navbar-item px-2"
:icon_size="36"
show_disabled_message
/>
<player-button-seek-forward
v-if="is_now_playing_page"
:seek_ms="30000"
class="navbar-item px-2"
:icon_size="24"
/>
<player-button-next
v-if="is_now_playing_page"
class="navbar-item px-2"
:icon_size="24"
/>
</control-link>
<control-player-play class="navbar-item" show_disabled_message />
</template>
<a
class="navbar-item ml-auto"
@click="show_player_menu = !show_player_menu"
class="navbar-item"
@click="uiStore.show_player_menu = !uiStore.show_player_menu"
>
<mdicon
class="icon"
:name="show_player_menu ? 'chevron-down' : 'chevron-up'"
:name="uiStore.show_player_menu ? 'chevron-down' : 'chevron-up'"
/>
</a>
</div>
<!-- Player menu for mobile and tablet -->
<div
class="navbar-menu is-hidden-desktop"
:class="{ 'is-active': show_player_menu }"
class="dropdown is-up is-right"
:class="{ 'is-active': uiStore.show_player_menu }"
>
<div class="navbar-item">
<div class="buttons has-addons is-centered">
<player-button-repeat class="button" />
<player-button-shuffle class="button" />
<player-button-consume class="button" />
<player-button-lyrics class="button" />
</div>
</div>
<hr class="my-3" />
<!-- Outputs: master volume -->
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a class="button is-white is-small" @click="toggle_mute_volume">
<mdicon
class="icon"
:name="player.volume > 0 ? 'volume-high' : 'volume-off'"
size="18"
/>
</a>
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<p class="heading" v-text="$t('navigation.volume')" />
<control-slider
v-model:value="player.volume"
:max="100"
@change="change_volume"
/>
</div>
</div>
</div>
</div>
</div>
<hr class="my-3" />
<!-- Outputs: speaker volumes -->
<navbar-item-output
v-for="output in outputs"
<div class="dropdown-menu">
<div class="dropdown-content">
<div class="dropdown-item pt-0">
<control-main-volume />
<control-output-volume
v-for="output in outputsStore.outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume -->
<hr class="my-3" />
<div class="navbar-item mb-5">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a
class="button is-white is-small"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
>
<mdicon class="icon" name="radio-tower" size="16" />
</a>
<control-stream-volume />
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<div
class="is-flex is-align-content-center"
:class="{ 'has-text-grey-light': !playing }"
>
<p class="heading" v-text="$t('navigation.stream')" />
<a href="stream.mp3" class="heading ml-2" target="_blank">
<mdicon
class="icon is-small"
name="open-in-new"
size="16"
/>
</a>
</div>
<control-slider
v-model:value="stream_volume"
:disabled="!playing"
:max="100"
:cursor="cursor"
@change="change_stream_volume"
/>
<hr class="dropdown-divider" />
<div class="dropdown-item is-flex is-justify-content-center">
<div class="buttons has-addons">
<control-player-repeat class="button" />
<control-player-shuffle class="button" />
<control-player-consume class="button" />
<control-player-lyrics class="button" />
</div>
</div>
</div>
@ -255,168 +73,66 @@
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import NavbarItemLink from '@/components/NavbarItemLink.vue'
import NavbarItemOutput from '@/components/NavbarItemOutput.vue'
import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue'
import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat.vue'
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack.vue'
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward.vue'
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle.vue'
import audio from '@/lib/Audio'
import { mdiCancel } from '@mdi/js'
import ControlLink from '@/components/ControlLink.vue'
import ControlMainVolume from '@/components/ControlMainVolume.vue'
import ControlOutputVolume from '@/components/ControlOutputVolume.vue'
import ControlPlayerBack from '@/components/ControlPlayerBack.vue'
import ControlPlayerConsume from '@/components/ControlPlayerConsume.vue'
import ControlPlayerForward from '@/components/ControlPlayerForward.vue'
import ControlPlayerLyrics from '@/components/ControlPlayerLyrics.vue'
import ControlPlayerNext from '@/components/ControlPlayerNext.vue'
import ControlPlayerPlay from '@/components/ControlPlayerPlay.vue'
import ControlPlayerPrevious from '@/components/ControlPlayerPrevious.vue'
import ControlPlayerRepeat from '@/components/ControlPlayerRepeat.vue'
import ControlPlayerShuffle from '@/components/ControlPlayerShuffle.vue'
import ControlStreamVolume from '@/components/ControlStreamVolume.vue'
import { useNotificationsStore } from '@/stores/notifications'
import { useOutputsStore } from '@/stores/outputs'
import { usePlayerStore } from '@/stores/player'
import { useQueueStore } from '@/stores/queue'
import { useUIStore } from '@/stores/ui'
import webapi from '@/webapi'
export default {
name: 'NavbarBottom',
components: {
ControlSlider,
NavbarItemLink,
NavbarItemOutput,
PlayerButtonConsume,
PlayerButtonLyrics,
PlayerButtonNext,
PlayerButtonPlayPause,
PlayerButtonPrevious,
PlayerButtonRepeat,
PlayerButtonSeekBack,
PlayerButtonSeekForward,
PlayerButtonShuffle
ControlLink,
ControlOutputVolume,
ControlMainVolume,
ControlPlayerBack,
ControlPlayerConsume,
ControlPlayerForward,
ControlPlayerLyrics,
ControlPlayerNext,
ControlPlayerPlay,
ControlPlayerPrevious,
ControlPlayerRepeat,
ControlPlayerShuffle,
ControlStreamVolume
},
setup() {
return {
notificationsStore: useNotificationsStore(),
outputsStore: useOutputsStore(),
playerStore: usePlayerStore(),
queueStore: useQueueStore(),
uiStore: useUIStore()
}
},
data() {
return {
cursor: mdiCancel,
loading: false,
old_volume: 0,
playing: false,
show_desktop_outputs_menu: false,
show_outputs_menu: false,
stream_volume: 10
}
},
computed: {
is_now_playing_page() {
return this.$route.name === 'now-playing'
},
current() {
return this.queueStore.current
},
outputs() {
return this.outputsStore.outputs
},
player() {
return this.playerStore
},
show_player_menu: {
get() {
return this.uiStore.show_player_menu
},
set(value) {
this.uiStore.show_player_menu = value
}
}
},
watch: {
'playerStore.volume'() {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
// On app mounted
mounted() {
this.setupAudio()
},
// On app destroyed
unmounted() {
this.closeAudio()
},
methods: {
change_stream_volume() {
audio.setVolume(this.stream_volume / 100)
},
change_volume() {
webapi.player_volume(this.player.volume)
},
closeAudio() {
audio.stop()
this.playing = false
},
on_click_outside_outputs() {
this.show_outputs_menu = false
},
playChannel() {
if (this.playing) {
return
}
this.loading = true
audio.play('/stream.mp3')
audio.setVolume(this.stream_volume / 100)
},
setupAudio() {
const a = audio.setup()
a.addEventListener('waiting', () => {
this.playing = false
this.loading = true
})
a.addEventListener('playing', () => {
this.playing = true
this.loading = false
})
a.addEventListener('ended', () => {
this.playing = false
this.loading = false
})
a.addEventListener('error', () => {
this.closeAudio()
this.notificationsStore.add({
text: this.$t('navigation.stream-error'),
type: 'danger'
})
this.playing = false
this.loading = false
})
},
togglePlay() {
if (this.loading) {
return
}
if (this.playing) {
this.closeAudio()
}
this.playChannel()
},
toggle_mute_volume() {
this.player.volume = this.player.volume > 0 ? 0 : this.old_volume
this.change_volume()
}
}
}
</script>
<style></style>
<style scoped>
.is-text-clipped {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -1,93 +0,0 @@
<template>
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a
class="button is-white is-small"
:class="{ 'has-text-grey-light': !output.selected }"
@click="set_enabled"
>
<mdicon
class="icon"
:name="type_class"
size="18"
:title="output.type"
/>
</a>
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<p
class="heading"
:class="{ 'has-text-grey-light': !output.selected }"
v-text="output.name"
/>
<control-slider
v-model:value="volume"
:disabled="!output.selected"
:max="100"
:cursor="cursor"
@change="change_volume"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import { mdiCancel } from '@mdi/js'
import webapi from '@/webapi'
export default {
name: 'NavbarItemOutput',
components: {
ControlSlider
},
props: { output: { required: true, type: Object } },
data() {
return {
cursor: mdiCancel,
volume: this.output.selected ? this.output.volume : 0
}
},
computed: {
type_class() {
if (this.output.type.startsWith('AirPlay')) {
return 'cast-variant'
} else if (this.output.type === 'Chromecast') {
return 'cast'
} else if (this.output.type === 'fifo') {
return 'pipe'
}
return 'server'
}
},
watch: {
output() {
this.volume = this.output.volume
}
},
methods: {
change_volume() {
webapi.player_output_volume(this.output.id, this.volume)
},
set_enabled() {
const values = {
selected: !this.output.selected
}
webapi.output_update(this.output.id, values)
}
}
}
</script>
<style></style>

View File

@ -1,135 +1,151 @@
<template>
<nav
class="navbar is-light is-fixed-top"
:style="zindex"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<navbar-item-link
<nav class="navbar is-light is-fixed-top" :style="zindex">
<div class="navbar-brand is-flex-grow-1">
<control-link
v-if="settingsStore.show_menu_item_playlists"
class="navbar-item"
:to="{ name: 'playlists' }"
>
<mdicon class="icon" name="music-box-multiple" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_music"
class="navbar-item"
:to="{ name: 'music' }"
>
<mdicon class="icon" name="music" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_podcasts"
class="navbar-item"
:to="{ name: 'podcasts' }"
>
<mdicon class="icon" name="microphone" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_audiobooks"
class="navbar-item"
:to="{ name: 'audiobooks' }"
>
<mdicon class="icon" name="book-open-variant" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_radio"
class="navbar-item"
:to="{ name: 'radio' }"
>
<mdicon class="icon" name="radio" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_files"
class="navbar-item"
:to="{ name: 'files' }"
>
<mdicon class="icon" name="folder-open" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_search"
class="navbar-item"
:to="{ name: searchStore.search_source }"
>
<mdicon class="icon" name="magnify" size="16" />
</navbar-item-link>
<div
class="navbar-burger"
:class="{ 'is-active': show_burger_menu }"
@click="show_burger_menu = !show_burger_menu"
</control-link>
<a
class="navbar-item ml-auto"
@click="uiStore.show_burger_menu = !uiStore.show_burger_menu"
>
<span />
<span />
<span />
</div>
</div>
<div class="navbar-menu" :class="{ 'is-active': show_burger_menu }">
<div class="navbar-start" />
<div class="navbar-end">
<!-- Burger menu entries -->
<div
class="navbar-item has-dropdown is-hoverable"
:class="{ 'is-active': show_settings_menu }"
@click="on_click_outside_settings"
>
<a class="navbar-item is-arrowless is-hidden-touch">
<mdicon class="icon" name="menu" size="24" />
<mdicon
class="icon"
:name="uiStore.show_burger_menu ? 'close' : 'menu'"
/>
</a>
<div class="navbar-dropdown is-right">
<navbar-item-link :to="{ name: 'playlists' }">
<div
class="dropdown is-right"
:class="{ 'is-active': uiStore.show_burger_menu }"
>
<div class="dropdown-menu">
<div class="dropdown-content">
<control-link class="dropdown-item" :to="{ name: 'playlists' }">
<span class="icon-text">
<mdicon class="icon" name="music-box-multiple" size="16" />
</span>
<b v-text="$t('navigation.playlists')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music' }">
<span class="icon-text">
<mdicon class="icon" name="music" size="16" />
</span>
<b v-text="$t('navigation.music')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music-artists' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music-artists' }">
<span class="pl-5" v-text="$t('navigation.artists')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music-albums' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music-albums' }">
<span class="pl-5" v-text="$t('navigation.albums')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music-genres' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music-genres' }">
<span class="pl-5" v-text="$t('navigation.genres')" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="spotify_enabled"
class="dropdown-item"
:to="{ name: 'music-spotify' }"
>
<span class="pl-5" v-text="$t('navigation.spotify')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'podcasts' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'podcasts' }">
<span class="icon-text">
<mdicon class="icon" name="microphone" size="16" />
</span>
<b v-text="$t('navigation.podcasts')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'audiobooks' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'audiobooks' }">
<span class="icon-text">
<mdicon class="icon" name="book-open-variant" size="16" />
</span>
<b v-text="$t('navigation.audiobooks')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'radio' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'radio' }">
<span class="icon-text">
<mdicon class="icon" name="radio" size="16" />
</span>
<b v-text="$t('navigation.radio')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'files' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'files' }">
<span class="icon-text">
<mdicon class="icon" name="folder-open" size="16" />
</span>
<b v-text="$t('navigation.files')" />
</navbar-item-link>
<navbar-item-link :to="{ name: searchStore.search_source }">
</control-link>
<control-link
class="dropdown-item"
:to="{ name: searchStore.search_source }"
>
<span class="icon-text">
<mdicon class="icon" name="magnify" size="16" />
</span>
<b v-text="$t('navigation.search')" />
</navbar-item-link>
</control-link>
<hr class="my-3" />
<navbar-item-link :to="{ name: 'settings-webinterface' }">
<control-link
class="dropdown-item"
:to="{ name: 'settings-webinterface' }"
>
{{ $t('navigation.settings') }}
</navbar-item-link>
</control-link>
<a
class="navbar-item"
class="dropdown-item"
@click.stop.prevent="open_update_dialog()"
v-text="$t('navigation.update-library')"
/>
<navbar-item-link :to="{ name: 'about' }">
<control-link class="dropdown-item" :to="{ name: 'about' }">
{{ $t('navigation.about') }}
</navbar-item-link>
</control-link>
</div>
</div>
</div>
</div>
<div
v-show="show_settings_menu"
class="is-overlay"
@ -139,7 +155,7 @@
</template>
<script>
import NavbarItemLink from '@/components/NavbarItemLink.vue'
import ControlLink from '@/components/ControlLink.vue'
import { useSearchStore } from '@/stores/search'
import { useServicesStore } from '@/stores/services'
import { useSettingsStore } from '@/stores/settings'
@ -147,7 +163,7 @@ import { useUIStore } from '@/stores/ui'
export default {
name: 'NavbarTop',
components: { NavbarItemLink },
components: { ControlLink },
setup() {
return {
@ -165,22 +181,6 @@ export default {
},
computed: {
show_burger_menu: {
get() {
return this.uiStore.show_burger_menu
},
set(value) {
this.uiStore.show_burger_menu = value
}
},
show_update_dialog: {
get() {
return this.uiStore.show_update_dialog
},
set(value) {
this.uiStore.show_update_dialog = value
}
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
},
@ -203,12 +203,10 @@ export default {
this.show_settings_menu = !this.show_settings_menu
},
open_update_dialog() {
this.show_update_dialog = true
this.uiStore.show_update_dialog = true
this.show_settings_menu = false
this.show_burger_menu = false
this.uiStore.show_burger_menu = false
}
}
}
</script>
<style></style>

View File

@ -1,108 +0,0 @@
<template>
<div class="field">
<input
:id="setting.name"
v-model="setting.value"
type="checkbox"
class="switch is-rounded mr-2"
@change="update_setting"
/>
<label class="pt-0" :for="setting.name">
<slot name="label" />
</label>
<i
class="is-size-7"
:class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
v-text="info"
/>
<p v-if="$slots['info']" class="help">
<slot name="info" />
</p>
</div>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'SettingsCheckbox',
props: {
category: { required: true, type: String },
name: { required: true, type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
statusUpdate: '',
timerDelay: 2000,
timerId: -1
}
},
computed: {
info() {
if (this.is_success) {
return this.$t('setting.saved')
} else if (this.is_error) {
return this.$t('setting.not-saved')
}
return ''
},
is_error() {
return this.statusUpdate === 'error'
},
is_success() {
return this.statusUpdate === 'success'
},
setting() {
const setting = this.settingsStore.setting(this.category, this.name)
if (!setting) {
return {
category: this.category,
name: this.name,
value: false
}
}
return setting
}
},
methods: {
clear_status() {
if (this.is_error) {
this.setting.value = !this.setting.value
}
this.statusUpdate = ''
},
update_setting() {
this.timerId = -1
const setting = {
category: this.category,
name: this.name,
value: this.setting.value
}
webapi
.settings_update(this.category, setting)
.then(() => {
this.settingsStore.update(setting)
this.statusUpdate = 'success'
})
.catch(() => {
this.statusUpdate = 'error'
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}
}
}
</script>
<style></style>

View File

@ -1,120 +0,0 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label class="label has-text-weight-normal">
<slot name="label" />
<i
class="is-size-7"
:class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
v-text="info"
/>
</label>
<div class="control">
<input
ref="setting"
class="column input is-one-fifth"
inputmode="numeric"
min="0"
:placeholder="placeholder"
:value="setting.value"
@input="set_update_timer"
/>
</div>
<p v-if="$slots['info']" class="help">
<slot name="info" />
</p>
</div>
</fieldset>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'SettingsIntfield',
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
statusUpdate: '',
timerDelay: 2000,
timerId: -1
}
},
computed: {
info() {
if (this.statusUpdate === 'success') {
return this.$t('setting.saved')
} else if (this.statusUpdate === 'error') {
return this.$t('setting.not-saved')
}
return ''
},
is_error() {
return this.statusUpdate === 'error'
},
is_success() {
return this.statusUpdate === 'success'
},
setting() {
return this.settingsStore.setting(this.category, this.name)
}
},
methods: {
clear_status() {
this.statusUpdate = ''
},
set_update_timer(event) {
event.target.value = event.target.value.replace(/[^0-9]/gu, '')
if (this.timerId > 0) {
window.clearTimeout(this.timerId)
this.timerId = -1
}
this.statusUpdate = ''
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
},
update_setting() {
this.timerId = -1
const newValue = parseInt(this.$refs.setting.value, 10)
if (isNaN(newValue) || newValue === this.value) {
this.statusUpdate = ''
return
}
const setting = {
category: this.category,
name: this.name,
value: newValue
}
webapi
.settings_update(this.category, setting)
.then(() => {
this.settingsStore.update(setting)
this.statusUpdate = 'success'
})
.catch(() => {
this.statusUpdate = 'error'
this.$refs.setting.value = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}
}
}
</script>
<style></style>

View File

@ -1,121 +0,0 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label class="label has-text-weight-normal">
<slot name="label" />
<i
class="is-size-7"
:class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
v-text="info"
/>
</label>
<div class="control">
<input
ref="setting"
class="input"
type="text"
:placeholder="placeholder"
:value="setting.value"
@input="set_update_timer"
/>
</div>
<p v-if="$slots['info']" class="help">
<slot name="info" />
</p>
</div>
</fieldset>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'SettingsTextfield',
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
statusUpdate: '',
timerDelay: 2000,
timerId: -1
}
},
computed: {
info() {
if (this.statusUpdate === 'success') {
return this.$t('setting.saved')
} else if (this.statusUpdate === 'error') {
return this.$t('setting.not-saved')
}
return ''
},
is_error() {
return this.statusUpdate === 'error'
},
is_success() {
return this.statusUpdate === 'success'
},
setting() {
return this.settingsStore.setting(this.category, this.name)
}
},
methods: {
clear_status() {
this.statusUpdate = ''
},
set_update_timer() {
if (this.timerId > 0) {
window.clearTimeout(this.timerId)
this.timerId = -1
}
this.statusUpdate = ''
const newValue = this.$refs.setting.value
if (newValue !== this.value) {
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
}
},
update_setting() {
this.timerId = -1
const newValue = this.$refs.setting.value
if (newValue === this.value) {
this.statusUpdate = ''
return
}
const setting = {
category: this.category,
name: this.name,
value: newValue
}
webapi
.settings_update(this.category, setting)
.then(() => {
this.settingsStore.update(setting)
this.statusUpdate = 'success'
})
.catch(() => {
this.statusUpdate = 'error'
this.$refs.setting.value = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<section class="section fd-tabs-section">
<section class="section tabs-section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
@ -67,5 +67,3 @@ export default {
name: 'TabsAudiobooks'
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<section class="section fd-tabs-section">
<section class="section tabs-section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
@ -129,5 +129,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -53,5 +53,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<section class="section fd-tabs-section">
<section class="section tabs-section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
@ -64,5 +64,3 @@ export default {
name: 'TabsSettings'
}
</script>
<style></style>

View File

@ -32,7 +32,7 @@ export const filters = {
.toLocaleString(DateTime.DATETIME_MED)
},
durationInDays(value) {
const minutes = Math.floor(value / 60000)
const minutes = Math.floor(value / 60)
if (minutes > 1440) {
return Duration.fromObject({ minutes })
.shiftTo('days', 'hours', 'minutes')
@ -53,6 +53,6 @@ export const filters = {
},
timeFromNow(value) {
const diff = DateTime.now().diff(DateTime.fromISO(value))
return this.durationInDays(diff.as('milliseconds'))
return this.durationInDays(diff.as('seconds'))
}
}

View File

@ -241,6 +241,7 @@
"compiled-with": "Compiliert mit Unterstützung von {options}.",
"library": "Bibliothek",
"more": "mehr",
"name": "Name",
"total-playtime": "Gesamte Spielzeit",
"tracks": "Tracks",
"update": "Neu einlesen",
@ -407,8 +408,8 @@
"count": "{count} Track|{count} Track|{count} Tracks",
"edit": "Bearbeiten",
"hide-previous": "Vorherige verbergen",
"title": "Warteschlange",
"save": "Speichern"
"save": "Speichern",
"title": "Warteschlange"
},
"radio": {
"count": "{count} Station|{count} Station|{count} Stationen",
@ -445,6 +446,7 @@
"discogs": "Discogs",
"explanation-1": "OwnTone verarbeitet PNG- und JPEG-Artwork, welches in einer eigenen Datei in der Bibliothek, in die Dateien eingebettet oder online von Radiostationen bereitgestellt werden kann.",
"explanation-2": "Zusätzlich kann auf folgende Artwork-Anbieter zugegriffen werden:",
"show-coverart": "Zeige Cover-Artwork in der Albumliste",
"spotify": "Spotify",
"streaming": "Bereitgestellte Artwork von Radiostationen ignorieren"
},
@ -460,7 +462,6 @@
"verify": "Verifizieren"
},
"general": {
"album-lists": "Album-Listen",
"audiobooks": "Hörbücher",
"files": "Dateien",
"genres": "Genres",
@ -473,17 +474,16 @@
"playlists": "Playlisten",
"podcasts": "Podcasts",
"radio": "Radio",
"recently-added-page-info": "Beschränkte die Zahl der Alben auf der \"kürzlich hinzugefügt\"-Seite",
"recently-added-page-info": "Beschränkte die Zahl der Alben",
"recently-added-page": "Kürzlich hinzugefügt-Seite",
"search": "Suche",
"show-composer-genres-info-1": "Komma-separierte Liste der Genres, wo der Komponist auf der \"Aktuell läuft\"-Seite angezeigt werden soll",
"show-composer-genres-info-2": "Leer lassen, um ihn immer anzuzeigen.",
"show-composer-genres-info-1": "Komma getrennte Liste der Genres, für die der Komponist angezeigt werden soll.",
"show-composer-genres-info-2": "Leer lassen, um den Komponisten immer anzuzeigen.",
"show-composer-genres-info-3": "Der Genre-Tag des aktuellen Tracks wird abgeglichen als Teil-String des Genre-Tags. Z.B. \"classical, soundtrack\" wird den Komponisten beim Genre-Tag \"Contemporary Classical\" anzeigen",
"show-composer-genres": "Zeige den Komponisten für die aufgelisteten Genres an",
"show-composer-info": "Wenn aktiviert, wird der Komponist auf der \"Aktuell läuft\"-Seite angezeigt.",
"show-composer-info": "Wenn aktiviert, wird der Komponist angezeigt.",
"show-composer": "Komponisten anzeigen",
"show-coverart": "Zeige Cover-Artwork in der Albumliste",
"show-path": "Dateipfad auf der \"Aktuell läuft\"-Seite anzeigen"
"show-path": "Dateipfad anzeigen"
},
"services": {
"lastfm": {
@ -495,8 +495,7 @@
"spotify": {
"no-support": "OwnTone wurde entweder ohne Unterstützung für Spotify erstellt oder libspotify ist nicht installiert.",
"logged-as": "Angemeldet als ",
"requirements": "Spotify Premium Abo erforderlich.",
"scopes": "Zugriff auf die Spotify Web-Api ermöglicht scannen der Spotify-Blibliothek. Erforderliche scopes sind: ",
"requirements": "Spotify Premium Abo erforderlich. Zugriff auf die Spotify Web-Api ermöglicht scannen der Spotify-Blibliothek. Erforderliche scopes sind: ",
"user": "Zugriff gestattet für ",
"authorize": "Authorisiere Web-API-Zugriff",
"grant-access": "Zugriff auf die Spotify Web-API gestatten",
@ -572,10 +571,6 @@
"toggle-lyrics": "Liedtexte anzeigen/verbergen"
}
},
"setting": {
"not-saved": " (Fehler beim Speichern der Einstellungen)",
"saved": " (Einstellungen gesichert)"
},
"server": {
"connection-failed": "Fehler bei Verbindung zum OwnTone-Server",
"request-failed": "Anfrage gescheitert (Status: {status} {cause} {url})",

View File

@ -241,6 +241,7 @@
"compiled-with": "Compiled with support for {options}.",
"library": "Library",
"more": "more",
"name": "Name",
"total-playtime": "Total playtime",
"tracks": "Tracks",
"update": "Update",
@ -407,8 +408,8 @@
"count": "{count} track|{count} track|{count} tracks",
"edit": "Edit",
"hide-previous": "Hide previous",
"title": "Queue",
"save": "Save"
"save": "Save",
"title": "Queue"
},
"radio": {
"count": "{count} station|{count} station|{count} stations",
@ -473,16 +474,16 @@
"playlists": "Playlists",
"podcasts": "Podcasts",
"radio": "Radio",
"recently-added-page-info": "Limit the number of albums shown on the \"Recently Added\" page",
"recently-added-page-info": "Limit the number of albums displayed",
"recently-added-page": "Recently added page",
"search": "Search",
"show-composer-genres-info-1": "Comma separated list of genres the composer should be displayed on the \"Now playing page\"",
"show-composer-genres-info-2": "Leave empty to always show the composer.",
"show-composer-genres-info-1": "Comma-separated list of genres, for which the composer must be displayed",
"show-composer-genres-info-2": "Leave empty to always show the composer",
"show-composer-genres-info-3": "The genre tag of the current track is matched by checking, if one of the defined genres are included. For example setting to \"classical, soundtrack\" will show the composer for tracks with a genre tag of \"Contemporary Classical\"",
"show-composer-genres": "Show composer only for listed genres",
"show-composer-info": "If enabled the composer of the current playing track is shown on the \"Now playing page\"",
"show-composer-info": "If enabled, the composer of the current playing track is displayed",
"show-composer": "Show composer",
"show-path": "Show filepath on the \"Now playing\" page"
"show-path": "Show filepath"
},
"services": {
"lastfm": {
@ -494,8 +495,7 @@
"spotify": {
"no-support": "OwnTone was either built without support for Spotify or libspotify is not installed.",
"logged-as": "Logged in as ",
"requirements": "You must have a Spotify premium account.",
"scopes": "Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are: ",
"requirements": "You must have a Spotify premium account. Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are: ",
"user": "Access granted for ",
"authorize": "Authorize Web API access",
"grant-access": "Grant access to the Spotify Web API",
@ -571,10 +571,6 @@
"toggle-lyrics": "Toggle lyrics"
}
},
"setting": {
"not-saved": " (error saving setting)",
"saved": " (setting saved)"
},
"server": {
"connection-failed": "Failed to connect to OwnTone server",
"request-failed": "Request failed (status: {status} {cause} {url})",

View File

@ -241,6 +241,7 @@
"compiled-with": "Compilé avec les options {options}.",
"library": "Bibliothèque",
"more": "plus",
"name": "Nom",
"total-playtime": "Durée totale de lecture",
"tracks": "Pistes",
"update": "Actualiser",
@ -407,8 +408,8 @@
"count": "{count} piste|{count} piste|{count} pistes",
"edit": "Éditer",
"hide-previous": "Masquer lhistorique",
"queue": "File dattente",
"save": "Enregistrer"
"save": "Enregistrer",
"title": "File dattente"
},
"radio": {
"count": "{count} station|{count} station|{count} stations",
@ -473,16 +474,16 @@
"playlists": "Listes de lecture",
"podcasts": "Podcasts",
"radio": "Radio",
"recently-added-page-info": "Limiter le nombre dalbums affichés dans la section « Ajouts récents »",
"recently-added-page-info": "Limiter le nombre dalbums affichés",
"recently-added-page": "Page « Ajouts récents »",
"search": "Recherche",
"show-composer-genres-info-1": "Liste des genres, séparés par des virgules, que le compositeur doit afficher sur la page « En cours de lecture ».",
"show-composer-genres-info-1": "Liste de genres séparés par des virgules pour lesquels le compositeur doit être affiché.",
"show-composer-genres-info-2": "Laissez vide pour toujours afficher le compositeur.",
"show-composer-genres-info-3": "Létiquette de genre de la piste actuelle est comparée en vérifiant si lun des genres définis est inclus. Par exemple, en choisissant \"classique, bande sonore\", le compositeur pour les pistes dont létiquette de genre est \"classique contemporain\" sera affiché.",
"show-composer-genres": "Afficher le compositeur uniquement pour les genres listés",
"show-composer-info": "Si actif, le compositeur de la piste en cours de lecture est affiché sur la page « En cours de lecture »",
"show-composer-info": "Si actif, le compositeur de la piste en cours de lecture est affiché",
"show-composer": "Afficher le compositeur",
"show-path": "Afficher le chemin du fichier sur la page « En cours de lecture »"
"show-path": "Afficher le chemin du fichier"
},
"services": {
"lastfm": {
@ -494,8 +495,7 @@
"spotify": {
"no-support": "Loption Spotify nest pas présente.",
"logged-as": "Connecté en tant que ",
"requirements": "Vous devez posséder un compte Spotify Premium.",
"scopes": "Laccès à lAPI de Spotify permet lanalyse de votre bibliothèque Spotify. Les champs dapplication requis sont les suivants :",
"requirements": "Vous devez posséder un compte Spotify Premium. Laccès à lAPI de Spotify permet lanalyse de votre bibliothèque Spotify. Les champs dapplication requis sont les suivants :",
"user": "Accès autorisé pour ",
"authorize": "Autoriser laccès à lAPI",
"grant-access": "Accordez laccès à lAPI de Spotify",
@ -571,10 +571,6 @@
"toggle-lyrics": "Voir/Cacher les paroles"
}
},
"setting": {
"not-saved": " (erreur à lenregistrement du réglage)",
"saved": " (réglage enregistré)"
},
"server": {
"connection-failed": "Échec de connexion au serveur",
"request-failed": "La requête a échoué (status: {status} {cause} {url})",

View File

@ -241,6 +241,7 @@
"compiled-with": "编译支持来自于 {options}",
"library": "资料库",
"more": "更多",
"name": "名称",
"total-playtime": "总播放时长",
"tracks": "曲目总数",
"update": "更新",
@ -407,8 +408,8 @@
"count": "{count} 只曲目|{count} 只曲目",
"edit": "编辑",
"hide-previous": "隐藏历史",
"title": "清单",
"save": "保存"
"save": "保存",
"title": "清单"
},
"radio": {
"count": "{count} 个电台|{count} 个电台",
@ -473,16 +474,16 @@
"playlists": "播放列表",
"podcasts": "播客",
"radio": "广播电台",
"recently-added-page-info": "限制“最近添加”页面上显示的专辑数量",
"recently-added-page-info": "限制显示的相册数量",
"recently-added-page": "“最近添加”页面",
"search": "搜索",
"show-composer-genres-info-1": "以逗号分隔流派,作曲家会在“正在播放的页面”上显示",
"show-composer-genres-info-1": "以逗号分隔的流派列表,必须显示作曲家所属的流派",
"show-composer-genres-info-2": "留空以始终显示作曲家",
"show-composer-genres-info-3": "通过检查是否包含定义的流派之一来匹配当前曲目的流派标签。例如,设置为“古典、原声带”将显示流派标签为“当代古典”的曲目的作曲家",
"show-composer-genres": "仅显示列出的流派的作曲家",
"show-composer-info": "如果启用,当前播放曲目的作曲家将显示在“正在播放页面”上",
"show-composer-info": "如果启用,则会显示当前播放曲目的作曲家",
"show-composer": "显示作曲家",
"show-path": "在“正在播放”页面显示文件路径"
"show-path": "显示文件路径"
},
"services": {
"lastfm": {
@ -494,8 +495,7 @@
"spotify": {
"no-support": "OwnTone的构建没有来自 Spotify 官方的支持,也未安装 libspotify",
"logged-as": "登录为 ",
"requirements": "您必须拥有 Spotify付费帐户",
"scopes": "访问 Spotify Web API 可以扫描您的 Spotify库。所需范围是",
"requirements": "您必须拥有 Spotify付费帐户。访问 Spotify Web API 可以扫描您的 Spotify库。所需范围是",
"user": "授予访问权限",
"authorize": "授权 Web API 访问",
"grant-access": "授予对 Spotify Web API 的访问权限",
@ -571,10 +571,6 @@
"toggle-lyrics": "显示/隐藏歌词"
}
},
"setting": {
"not-saved": " (设置保存错误)",
"saved": " (设置已保存)"
},
"server": {
"connection-failed": "无法连接到 OwnTone 服务器",
"request-failed": "请求失败 (状态:{status} {cause} {url})",

View File

@ -241,6 +241,7 @@
"compiled-with": "編譯支持來自於 {options}",
"library": "資料庫",
"more": "更多",
"name": "名稱",
"total-playtime": "總播放時長",
"tracks": "曲目總數",
"update": "更新",
@ -407,8 +408,8 @@
"count": "{count} 首曲目|{count} 首曲目",
"edit": "編輯",
"hide-previous": "隱藏歷史",
"title": "清單",
"save": "儲存"
"save": "儲存",
"title": "清單"
},
"radio": {
"count": "{count} 個電台|{count} 個電台",
@ -473,16 +474,16 @@
"playlists": "播放列表",
"podcasts": "Podcast",
"radio": "電台",
"recently-added-page-info": "限制“最近新增”頁面上顯示的專輯數量",
"recently-added-page-info": "限制顯示的相簿數量",
"recently-added-page": "“最近新增”頁面",
"search": "搜尋",
"show-composer-genres-info-1": "以逗號分隔音樂類型,作曲家會在“正在播放的頁面”上顯示",
"show-composer-genres-info-1": "以逗號分隔的音樂類型清單,將顯示作曲家的音樂類型",
"show-composer-genres-info-2": "留空以始終顯示作曲家",
"show-composer-genres-info-3": "通過檢查是否包含定義的音樂類型之一來匹配當前曲目的音樂類型標籤。例如,設定為“古典、原聲帶”將顯示音樂類型標籤為“當代古典”的曲目的作曲家",
"show-composer-genres": "僅顯示列出的音樂類型的作曲家",
"show-composer-info": "如果啓用,當前播放曲目的作曲家將顯示在“正在播放頁面”上",
"show-composer-info": "如果啟用,則會顯示目前播放曲目的作曲家",
"show-composer": "顯示作曲家",
"show-path": "在“正在播放”頁面顯示文件路徑"
"show-path": "顯示檔案路徑"
},
"services": {
"lastfm": {
@ -494,8 +495,7 @@
"spotify": {
"no-support": "OwnTone並無 Spotify 官方的支持,也未安裝 libspotify",
"logged-as": "登入為 ",
"requirements": "您必須擁有 Spotify付費帳戶",
"scopes": "訪問 Spotify Web API 可以掃描您的 Spotify庫。所需範圍是",
"requirements": "您必須擁有 Spotify付費帳戶。訪問 Spotify Web API 可以掃描您的 Spotify庫。所需範圍是",
"user": "授予訪問權限",
"authorize": "授權 Web API 訪問",
"grant-access": "授予對 Spotify Web API 的訪問權限",
@ -571,10 +571,6 @@
"toggle-lyrics": "顯示/隱藏歌詞"
}
},
"setting": {
"not-saved": " (設定儲存錯誤)",
"saved": " (設定已儲存)"
},
"server": {
"connection-failed": "無法連接到 OwnTone 伺服器",
"request-failed": "請求失敗 (狀態:{status} {cause} {url})",

View File

@ -14,6 +14,7 @@ import {
mdiChevronDown,
mdiChevronLeft,
mdiChevronUp,
mdiClose,
mdiContentSave,
mdiDelete,
mdiDeleteEmpty,
@ -79,6 +80,7 @@ export const icons = {
mdiChevronDown,
mdiChevronLeft,
mdiChevronUp,
mdiClose,
mdiContentSave,
mdiDelete,
mdiDeleteEmpty,

View File

@ -3,7 +3,7 @@
@use 'bulma/bulma';
@use 'bulma/sass/utilities/mixins';
.fd-tabs-section {
.tabs-section {
padding-bottom: 0;
padding-top: 0;
background: var(--bulma-body-background-color);

View File

@ -1,27 +1,15 @@
<template>
<div>
<section class="section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths has-text-centered-mobile">
<h1 class="title is-4" v-text="configuration.library_name" />
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<div class="content">
<nav class="level is-mobile">
<!-- Left side -->
<nav class="level">
<div class="level-left">
<div class="level-item">
<p class="title is-5" v-text="$t('page.about.library')" />
<p class="title is-4" v-text="$t('page.about.library')" />
</div>
</div>
<!-- Right side -->
<div class="level-right">
<div v-if="library.updating">
<a
@ -38,56 +26,59 @@
</div>
</div>
</nav>
<table class="table">
<tbody>
<tr>
<th
class="has-text-left"
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.name')"
/>
<div class="media-right" v-text="configuration.library_name" />
</div>
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.artists')"
/>
<td
class="has-text-right"
<div
class="media-right"
v-text="$filters.number(library.artists)"
/>
</tr>
<tr>
<th
class="has-text-left"
</div>
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.albums')"
/>
<td
class="has-text-right"
<div
media="media-right"
v-text="$filters.number(library.albums)"
/>
</tr>
<tr>
<th
class="has-text-left"
</div>
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.tracks')"
/>
<td
class="has-text-right"
<div
class="media-right"
v-text="$filters.number(library.songs)"
/>
</tr>
<tr>
<th
class="has-text-left"
</div>
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.total-playtime')"
/>
<td
class="has-text-right"
v-text="
$filters.durationInDays(library.db_playtime * 1000)
"
<div
class="media-right"
v-text="$filters.durationInDays(library.db_playtime)"
/>
</tr>
<tr>
<th
class="has-text-left"
</div>
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.updated')"
/>
<td class="has-text-right">
<div class="media-right">
<span
v-text="
$t('page.about.updated-on', {
@ -99,25 +90,21 @@
class="has-text-grey"
v-text="$filters.datetime(library.updated_at)"
/>)
</td>
</tr>
<tr>
<th
class="has-text-left"
</div>
</div>
<div class="media">
<div
class="media-content has-text-weight-bold"
v-text="$t('page.about.uptime')"
/>
<td class="has-text-right">
<span
v-text="$filters.timeFromNow(library.started_at, true)"
/>
<div class="media-right">
<span v-text="$filters.timeFromNow(library.started_at, true)" />
(<span
class="has-text-grey"
v-text="$filters.datetime(library.started_at)"
/>)
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@ -174,7 +161,6 @@
</div>
</div>
</section>
</div>
</template>
<script>
@ -209,5 +195,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -2,17 +2,20 @@
<div>
<content-with-hero>
<template #heading-left>
<h1 class="title is-5" v-text="album.name" />
<h2 class="subtitle is-6 has-text-link">
<a class="has-text-link" @click="open_artist" v-text="album.artist" />
</h2>
<div class="buttons fd-is-centered-mobile mt-5">
<a class="button is-small is-dark is-rounded" @click="play">
<div class="title is-5" v-text="album.name" />
<div class="subtitle is-6">
<a @click="open_artist" v-text="album.artist" />
</div>
<div class="buttons is-centered-mobile mt-5">
<a
class="button has-background-light is-small is-rounded"
@click="play"
>
<mdicon class="icon" name="shuffle" size="16" />
<span v-text="$t('page.album.shuffle')" />
</a>
<a
class="button is-small is-light is-rounded"
class="button is-small has-background-light is-rounded"
@click="show_details_modal = true"
>
<mdicon class="icon" name="dots-horizontal" size="16" />
@ -29,8 +32,8 @@
/>
</template>
<template #content>
<p
class="heading has-text-centered-mobile mt-5"
<div
class="is-size-7 is-uppercase has-text-centered-mobile my-5"
v-text="$t('page.album.track-count', { count: album.track_count })"
/>
<list-tracks :items="tracks" :uris="album.uri" />
@ -104,5 +107,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -2,15 +2,11 @@
<div>
<content-with-hero>
<template #heading-left>
<h1 class="title is-5" v-text="album.name" />
<h2 class="subtitle is-6 has-text-link">
<a
class="has-text-link"
@click="open_artist"
v-text="album.artists[0].name"
/>
</h2>
<div class="buttons fd-is-centered-mobile mt-5">
<div class="title is-5" v-text="album.name" />
<div class="subtitle is-6">
<a @click="open_artist" v-text="album.artists[0].name" />
</div>
<div class="buttons is-centered-mobile mt-5">
<a class="button is-small is-dark is-rounded" @click="play">
<mdicon class="icon" name="shuffle" size="16" />
<span v-text="$t('page.spotify.album.shuffle')" />
@ -33,8 +29,8 @@
/>
</template>
<template #content>
<p
class="heading has-text-centered-mobile mt-5"
<div
class="is-size-7 is-uppercase has-text-centered-mobile mt-5"
v-text="
$t('page.spotify.album.track-count', { count: album.tracks.total })
"
@ -127,5 +123,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,60 +1,54 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #options>
<index-button-list :indices="albums.indices" />
<div class="columns">
<div class="column">
<p class="heading mb-5" v-text="$t('page.albums.filter')" />
<div class="field">
<div class="control">
<input
id="switchHideSingles"
v-model="hide_singles"
type="checkbox"
class="switch is-rounded"
<div
class="is-size-7 is-uppercase"
v-text="$t('page.albums.filter')"
/>
<label
for="switchHideSingles"
v-text="$t('page.albums.hide-singles')"
/>
</div>
<p class="help" v-text="$t('page.albums.hide-singles-help')" />
</div>
<div v-if="spotify_enabled" class="field">
<div class="control">
<input
id="switchHideSpotify"
v-model="hide_spotify"
type="checkbox"
class="switch is-rounded"
/>
<label
for="switchHideSpotify"
v-text="$t('page.albums.hide-spotify')"
/>
</div>
<p class="help" v-text="$t('page.albums.hide-spotify-help')" />
</div>
<control-switch v-model="uiStore.hide_singles">
<template #label>
<span v-text="$t('page.albums.hide-singles')" />
</template>
<template #help>
<span v-text="$t('page.albums.hide-singles-help')" />
</template>
</control-switch>
<control-switch
v-if="spotify_enabled"
v-model="uiStore.hide_spotify"
>
<template #label>
<span v-text="$t('page.albums.hide-spotify')" />
</template>
<template #help>
<span v-text="$t('page.albums.hide-spotify-help')" />
</template>
</control-switch>
</div>
<div class="column">
<p class="heading mb-5" v-text="$t('page.albums.sort.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.albums.sort.title')"
/>
<control-dropdown
v-model:value="selected_grouping_id"
v-model:value="uiStore.albums_sort"
:options="groupings"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4" v-text="$t('page.albums.title')" />
<p
class="heading"
<div class="title is-4" v-text="$t('page.albums.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.albums.count', { count: albums.count })"
/>
</template>
<template #heading-right />
<template #content>
<list-albums :items="albums" />
</template>
@ -65,6 +59,7 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListAlbums from '@/components/ListAlbums.vue'
@ -88,6 +83,7 @@ export default {
components: {
ContentWithHeading,
ControlDropdown,
ControlSwitch,
IndexButtonList,
ListAlbums,
TabsMusic
@ -157,43 +153,17 @@ export default {
computed: {
albums() {
const { options } = this.groupings.find(
(grouping) => grouping.id === this.selected_grouping_id
(grouping) => grouping.id === this.uiStore.albums_sort
)
options.filters = [
(album) => !this.hide_singles || album.track_count > 2,
(album) => !this.hide_spotify || album.data_kind !== 'spotify'
(album) => !this.uiStore.hide_singles || album.track_count > 2,
(album) => !this.uiStore.hide_spotify || album.data_kind !== 'spotify'
]
return this.albums_list.group(options)
},
hide_singles: {
get() {
return this.uiStore.hide_singles
},
set(value) {
this.uiStore.hide_singles = value
}
},
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.albums_sort
},
set(value) {
this.uiStore.albums_sort = value
}
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
}
}
}
</script>
<style></style>

View File

@ -4,34 +4,46 @@
<template #options>
<div class="columns">
<div class="column">
<p class="heading mb-5" v-text="$t('page.artist.filter')" />
<div v-if="spotify_enabled" class="field">
<div class="control">
<input
id="switchHideSpotify"
v-model="hide_spotify"
type="checkbox"
class="switch is-rounded"
<div
class="is-size-7 is-uppercase"
v-text="$t('page.artist.filter')"
/>
<label
for="switchHideSpotify"
v-text="$t('page.artist.hide-spotify')"
/>
</div>
<p class="help" v-text="$t('page.artist.hide-spotify-help')" />
</div>
<control-switch
v-if="spotify_enabled"
v-model="uiStore.hide_spotify"
>
<template #label>
<span v-text="$t('page.artist.hide-spotify')" />
</template>
<template #help>
<span v-text="$t('page.artist.hide-spotify-help')" />
</template>
</control-switch>
</div>
<div class="column">
<p class="heading mb-5" v-text="$t('page.artist.sort.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.artist.sort.title')"
/>
<control-dropdown
v-model:value="selected_grouping_id"
v-model:value="uiStore.artist_albums_sort"
:options="groupings"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4" v-text="artist.name" />
<div class="title is-4" v-text="artist.name" />
<div class="is-size-7 is-uppercase">
<span
v-text="$t('page.artist.album-count', { count: albums.count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
@click="open_tracks"
v-text="$t('page.artist.track-count', { count: track_count })"
/>
</div>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -48,17 +60,6 @@
</div>
</template>
<template #content>
<p class="heading has-text-centered-mobile">
<span
v-text="$t('page.artist.album-count', { count: albums.count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
class="has-text-link"
@click="open_tracks"
v-text="$t('page.artist.track-count', { count: track_count })"
/>
</p>
<list-albums :items="albums" />
<modal-dialog-artist
:item="artist"
@ -73,6 +74,7 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList'
import ListAlbums from '@/components/ListAlbums.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
@ -99,6 +101,7 @@ export default {
components: {
ContentWithHeading,
ControlDropdown,
ControlSwitch,
ListAlbums,
ModalDialogArtist
},
@ -136,29 +139,13 @@ export default {
computed: {
albums() {
const { options } = this.groupings.find(
(grouping) => grouping.id === this.selected_grouping_id
(grouping) => grouping.id === this.uiStore.artist_albums_sort
)
options.filters = [
(album) => !this.hide_spotify || album.data_kind !== 'spotify'
(album) => !this.uiStore.hide_spotify || album.data_kind !== 'spotify'
]
return this.albums_list.group(options)
},
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.artist_albums_sort
},
set(value) {
this.uiStore.artist_albums_sort = value
}
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
},
@ -187,5 +174,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -2,7 +2,11 @@
<div>
<content-with-heading>
<template #heading-left>
<p class="title is-4" v-text="artist.name" />
<div class="title is-4" v-text="artist.name" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.spotify.artist.album-count', { count: total })"
/>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -19,10 +23,6 @@
</div>
</template>
<template #content>
<p
class="heading has-text-centered-mobile"
v-text="$t('page.spotify.artist.album-count', { count: total })"
/>
<list-albums-spotify :items="albums" />
<VueEternalLoading v-if="offset < total" :load="load_next">
<template #loading>
@ -140,5 +140,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -5,27 +5,29 @@
<index-button-list :indices="tracks.indices" />
<div class="columns">
<div class="column">
<p class="heading mb-5" v-text="$t('page.artist.filter')" />
<div v-if="spotify_enabled" class="field">
<div class="control">
<input
id="switchHideSpotify"
v-model="hide_spotify"
type="checkbox"
class="switch is-rounded"
<p
class="is-size-7 is-uppercase"
v-text="$t('page.artist.filter')"
/>
<label
for="switchHideSpotify"
v-text="$t('page.artist.hide-spotify')"
/>
</div>
<p class="help" v-text="$t('page.artist.hide-spotify-help')" />
</div>
<control-switch
v-if="spotify_enabled"
v-model="uiStore.hide_spotify"
>
<template #label>
<span v-text="$t('page.artist.hide-spotify')" />
</template>
<template #help>
<span v-text="$t('page.artist.hide-spotify-help')" />
</template>
</control-switch>
</div>
<div class="column">
<p class="heading mb-5" v-text="$t('page.artist.sort.title')" />
<p
class="is-size-7 is-uppercase"
v-text="$t('page.artist.sort.title')"
/>
<control-dropdown
v-model:value="selected_grouping_id"
v-model:value="uiStore.artist_tracks_sort"
:options="groupings"
/>
</div>
@ -33,6 +35,16 @@
</template>
<template #heading-left>
<p class="title is-4" v-text="artist.name" />
<div class="is-size-7 is-uppercase">
<a
@click="open_artist"
v-text="$t('page.artist.album-count', { count: album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.artist.track-count', { count: tracks.count })"
/>
</div>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -49,17 +61,6 @@
</div>
</template>
<template #content>
<p class="heading has-text-centered-mobile">
<a
class="has-text-link"
@click="open_artist"
v-text="$t('page.artist.album-count', { count: album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.artist.track-count', { count: tracks.count })"
/>
</p>
<list-tracks :items="tracks" :uris="track_uris" />
<modal-dialog-artist
:item="artist"
@ -74,6 +75,7 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListTracks from '@/components/ListTracks.vue'
@ -101,6 +103,7 @@ export default {
components: {
ContentWithHeading,
ControlDropdown,
ControlSwitch,
IndexButtonList,
ListTracks,
ModalDialogArtist
@ -147,22 +150,6 @@ export default {
.map((track) => track.item.album_id)
).size
},
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.artist_tracks_sort
},
set(value) {
this.uiStore.artist_tracks_sort = value
}
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
},
@ -171,10 +158,10 @@ export default {
},
tracks() {
const { options } = this.groupings.find(
(grouping) => grouping.id === this.selected_grouping_id
(grouping) => grouping.id === this.uiStore.artist_tracks_sort
)
options.filters = [
(track) => !this.hide_spotify || track.data_kind !== 'spotify'
(track) => !this.uiStore.hide_spotify || track.data_kind !== 'spotify'
]
return this.tracks_list.group(options)
}
@ -197,5 +184,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,60 +1,53 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #options>
<index-button-list :indices="artists.indices" />
<div class="columns">
<div class="column">
<p class="heading mb-5" v-text="$t('page.artists.filter')" />
<div class="field">
<div class="control">
<input
id="switchHideSingles"
v-model="hide_singles"
type="checkbox"
class="switch is-rounded"
<div
class="is-size-7 is-uppercase"
v-text="$t('page.artists.filter')"
/>
<label
for="switchHideSingles"
v-text="$t('page.artists.hide-singles')"
/>
</div>
<p class="help" v-text="$t('page.artists.hide-singles-help')" />
</div>
<control-switch v-model="uiStore.hide_singles">
<template #label>
<span v-text="$t('page.artists.hide-singles')" />
</template>
<template #help>
<span v-text="$t('page.artists.hide-singles-help')" />
</template>
</control-switch>
<div v-if="spotify_enabled" class="field">
<div class="control">
<input
id="switchHideSpotify"
v-model="hide_spotify"
type="checkbox"
class="switch is-rounded"
/>
<label
for="switchHideSpotify"
v-text="$t('page.artists.hide-spotify')"
/>
</div>
<p class="help" v-text="$t('page.artists.hide-spotify-help')" />
<control-switch v-model="uiStore.hide_spotify">
<template #label>
<span v-text="$t('page.artists.hide-spotify')" />
</template>
<template #help>
<span v-text="$t('page.artists.hide-spotify-help')" />
</template>
</control-switch>
</div>
</div>
<div class="column">
<p class="heading mb-5" v-text="$t('page.artists.sort.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.artists.sort.title')"
/>
<control-dropdown
v-model:value="selected_grouping_id"
v-model:value="uiStore.artists_sort"
:options="groupings"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4" v-text="$t('page.artists.title')" />
<p
class="heading"
<div class="title is-4" v-text="$t('page.artists.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.artists.count', { count: artists.count })"
/>
</template>
<template #heading-right />
<template #content>
<list-artists :items="artists" />
</template>
@ -65,6 +58,7 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListArtists from '@/components/ListArtists.vue'
@ -88,6 +82,7 @@ export default {
components: {
ContentWithHeading,
ControlDropdown,
ControlSwitch,
IndexButtonList,
ListArtists,
TabsMusic
@ -125,47 +120,21 @@ export default {
},
computed: {
// Wraps GroupedList and updates it if filter or sort changes
artists() {
const { options } = this.groupings.find(
(grouping) => grouping.id === this.selected_grouping_id
(grouping) => grouping.id === this.uiStore.artists_sort
)
options.filters = [
(artist) =>
!this.hide_singles || artist.track_count > artist.album_count * 2,
(artist) => !this.hide_spotify || artist.data_kind !== 'spotify'
!this.uiStore.hide_singles ||
artist.track_count > artist.album_count * 2,
(artist) => !this.uiStore.hide_spotify || artist.data_kind !== 'spotify'
]
return this.artists_list.group(options)
},
hide_singles: {
get() {
return this.uiStore.hide_singles
},
set(value) {
this.uiStore.hide_singles = value
}
},
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.artists_sort
},
set(value) {
this.uiStore.artists_sort = value
}
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
}
}
}
</script>
<style></style>

View File

@ -2,11 +2,11 @@
<div>
<content-with-hero>
<template #heading-left>
<h1 class="title is-5" v-text="album.name" />
<h2 class="subtitle is-6 has-text-link">
<a class="has-text-link" @click="open_artist" v-text="album.artist" />
</h2>
<div class="buttons fd-is-centered-mobile mt-5">
<div class="title is-5" v-text="album.name" />
<div class="subtitle is-6">
<a @click="open_artist" v-text="album.artist" />
</div>
<div class="buttons is-centered-mobile mt-5">
<a class="button is-small is-dark is-rounded" @click="play">
<mdicon class="icon" name="play" size="16" />
<span v-text="$t('page.audiobooks.album.play')" />
@ -29,8 +29,8 @@
/>
</template>
<template #content>
<p
class="heading has-text-centered-mobile mt-5"
<div
class="is-size-7 is-uppercase has-text-centered-mobile mt-5"
v-text="
$t('page.audiobooks.album.track-count', {
count: album.track_count
@ -103,5 +103,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-audiobooks />
<content-with-heading>
<template #options>
@ -8,7 +8,7 @@
<template #heading-left>
<p class="title is-4" v-text="$t('page.audiobooks.albums.title')" />
<p
class="heading"
class="is-size-7 is-uppercase"
v-text="$t('page.audiobooks.albums.count', { count: albums.count })"
/>
</template>
@ -61,5 +61,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -2,7 +2,15 @@
<div>
<content-with-heading>
<template #heading-left>
<p class="title is-4" v-text="artist.name" />
<div class="title is-4" v-text="artist.name" />
<div
class="is-size-7 is-uppercase"
v-text="
$t('page.audiobooks.artist.album-count', {
count: artist.album_count
})
"
/>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -19,14 +27,6 @@
</div>
</template>
<template #content>
<p
class="heading has-text-centered-mobile"
v-text="
$t('page.audiobooks.artist.album-count', {
count: artist.album_count
})
"
/>
<list-albums :items="albums" />
<modal-dialog-artist
:item="artist"
@ -87,5 +87,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,18 +1,17 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-audiobooks />
<content-with-heading>
<template #options>
<index-button-list :indices="artists.indices" />
</template>
<template #heading-left>
<p class="title is-4" v-text="$t('page.audiobooks.artists.title')" />
<p
class="heading"
<div class="title is-4" v-text="$t('page.audiobooks.artists.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.audiobooks.artists.count', { count: artists.count })"
/>
</template>
<template #heading-right />
<template #content>
<list-artists :items="artists" />
</template>
@ -62,5 +61,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,14 +1,14 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-audiobooks />
<content-with-heading>
<template #options>
<index-button-list :indices="genres.indices" />
</template>
<template #heading-left>
<p class="title is-4" v-text="$t('page.genres.title')" />
<p
class="heading"
<div class="title is-4" v-text="$t('page.genres.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.genres.count', { count: genres.total })"
/>
</template>
@ -61,5 +61,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -2,7 +2,21 @@
<div>
<content-with-heading>
<template #heading-left>
<p class="title is-4" v-text="composer.name" />
<div class="title is-4" v-text="composer.name" />
<div class="is-size-7 is-uppercase">
<span
v-text="
$t('page.composer.album-count', { count: composer.album_count })
"
/>
<span>&nbsp;|&nbsp;</span>
<a
@click="open_tracks"
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</div>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -19,21 +33,6 @@
</div>
</template>
<template #content>
<p class="heading has-text-centered-mobile">
<span
v-text="
$t('page.composer.album-count', { count: composer.album_count })
"
/>
<span>&nbsp;|&nbsp;</span>
<a
class="has-text-link"
@click="open_tracks"
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</p>
<list-albums :items="albums" />
<modal-dialog-composer
:item="composer"
@ -104,5 +103,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -5,16 +5,35 @@
<index-button-list :indices="tracks.indices" />
<div class="columns">
<div class="column">
<p class="heading mb-5" v-text="$t('page.artist.sort.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.artist.sort.title')"
/>
<control-dropdown
v-model:value="selected_grouping_id"
v-model:value="uiStore.composer_tracks_sort"
:options="groupings"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4" v-text="composer.name" />
<div class="title is-4" v-text="composer.name" />
<div class="is-size-7 is-uppercase">
<a
@click="open_albums"
v-text="
$t('page.composer.album-count', {
count: composer.album_count
})
"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</div>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -31,23 +50,6 @@
</div>
</template>
<template #content>
<p class="heading has-text-centered-mobile">
<a
class="has-text-link"
@click="open_albums"
v-text="
$t('page.composer.album-count', {
count: composer.album_count
})
"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</p>
<list-tracks :items="tracks" :expression="expression" />
<modal-dialog-composer
:item="composer"
@ -130,17 +132,9 @@ export default {
expression() {
return `composer is "${this.composer.name}" and media_kind is music`
},
selected_grouping_id: {
get() {
return this.uiStore.composer_tracks_sort
},
set(value) {
this.uiStore.composer_tracks_sort = value
}
},
tracks() {
const { options } = this.groupings.find(
(grouping) => grouping.id === this.selected_grouping_id
(grouping) => grouping.id === this.uiStore.composer_tracks_sort
)
return this.tracks_list.group(options)
}
@ -160,5 +154,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,14 +1,14 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #options>
<index-button-list :indices="composers.indices" />
</template>
<template #heading-left>
<p class="title is-4" v-text="$t('page.composers.title')" />
<p
class="heading"
<div class="title is-4" v-text="$t('page.composers.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.composers.count', { count: composers.total })"
/>
</template>
@ -56,5 +56,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -137,5 +137,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -5,7 +5,17 @@
<index-button-list :indices="albums.indices" />
</template>
<template #heading-left>
<p class="title is-4" v-text="genre.name" />
<div class="title is-4" v-text="genre.name" />
<div class="is-size-7 is-uppercase">
<span
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
@click="open_tracks"
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</div>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -22,17 +32,6 @@
</div>
</template>
<template #content>
<p class="heading has-text-centered-mobile">
<span
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
class="has-text-link"
@click="open_tracks"
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</p>
<list-albums :items="albums" />
<modal-dialog-genre
:item="genre"
@ -109,5 +108,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -5,16 +5,29 @@
<index-button-list :indices="tracks.indices" />
<div class="columns">
<div class="column">
<p class="heading mb-5" v-text="$t('page.genre.sort.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.genre.sort.title')"
/>
<control-dropdown
v-model:value="selected_grouping_id"
v-model:value="uiStore.genre_tracks_sort"
:options="groupings"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4" v-text="genre.name" />
<div class="title is-4" v-text="genre.name" />
<div class="is-size-7 is-uppercase">
<a
@click="open_genre"
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</div>
</template>
<template #heading-right>
<div class="buttons is-centered">
@ -31,17 +44,6 @@
</div>
</template>
<template #content>
<p class="heading has-text-centered-mobile">
<a
class="has-text-link"
@click="open_genre"
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</p>
<list-tracks :items="tracks" :expression="expression" />
<modal-dialog-genre
:item="genre"
@ -126,17 +128,9 @@ export default {
expression() {
return `genre is "${this.genre.name}" and media_kind is ${this.media_kind}`
},
selected_grouping_id: {
get() {
return this.uiStore.genre_tracks_sort
},
set(value) {
this.uiStore.genre_tracks_sort = value
}
},
tracks() {
const { options } = this.groupings.find(
(grouping) => grouping.id === this.selected_grouping_id
(grouping) => grouping.id === this.uiStore.genre_tracks_sort
)
return this.tracks_list.group(options)
}
@ -157,5 +151,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,14 +1,14 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #options>
<index-button-list :indices="genres.indices" />
</template>
<template #heading-left>
<p class="title is-4" v-text="$t('page.genres.title')" />
<p
class="heading"
<div class="title is-4" v-text="$t('page.genres.title')" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.genres.count', { count: genres.total })"
/>
</template>
@ -61,5 +61,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #heading-left>
@ -12,7 +12,7 @@
<nav class="level">
<p class="level-item">
<router-link
class="button is-light is-small is-rounded"
class="button is-small is-rounded"
:to="{ name: 'music-recently-added' }"
>
{{ $t('page.music.show-more') }}
@ -32,7 +32,7 @@
<nav class="level">
<p class="level-item">
<router-link
class="button is-light is-small is-rounded"
class="button is-small is-rounded"
:to="{ name: 'music-recently-played' }"
>
{{ $t('page.music.show-more') }}
@ -95,5 +95,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #heading-left>
@ -62,5 +62,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #heading-left>
@ -51,5 +51,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #heading-left>
@ -13,7 +13,7 @@
<p class="level-item">
<router-link
:to="{ name: 'music-spotify-new-releases' }"
class="button is-light is-small is-rounded"
class="button is-small is-rounded"
>
{{ $t('page.spotify.music.show-more') }}
</router-link>
@ -36,7 +36,7 @@
<p class="level-item">
<router-link
:to="{ name: 'music-spotify-featured-playlists' }"
class="button is-light is-small is-rounded"
class="button is-small is-rounded"
>
{{ $t('page.spotify.music.show-more') }}
</router-link>
@ -102,5 +102,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #heading-left>
@ -60,5 +60,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,5 +1,5 @@
<template>
<div class="fd-page-with-tabs">
<div>
<tabs-music />
<content-with-heading>
<template #heading-left>
@ -57,5 +57,3 @@ export default {
}
}
</script>
<style></style>

View File

@ -1,7 +1,8 @@
<template>
<div class="hero is-full-height">
<div v-if="track.id > 0" class="hero-body is-flex is-align-items-center">
<div class="container has-text-centered" style="max-width: 500px">
<div class="hero-body is-flex is-align-items-center">
<div class="container has-text-centered">
<div v-if="track.id" class="mx-auto" style="max-width: 32rem">
<cover-artwork
:url="track.artwork_url"
:artist="track.artist"
@ -39,9 +40,7 @@
v-text="track.path"
/>
</div>
</div>
<div v-else class="hero-body is-flex is-align-items-center">
<div class="container has-text-centered">
<div v-else>
<p class="title is-5" v-text="$t('page.now-playing.title')" />
<p class="subtitle" v-text="$t('page.now-playing.info')" />
</div>
@ -52,6 +51,7 @@
@close="show_details_modal = false"
/>
</div>
</div>
</template>
<script>
@ -196,5 +196,3 @@ export default {
}
}
</script>
<style></style>

Some files were not shown because too many files have changed in this diff Show More