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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<template> <template>
<a class="navbar-item" :href="href" @click.stop.prevent="open"> <a :href="href" @click.stop.prevent="open">
<slot /> <slot />
</a> </a>
</template> </template>
@ -8,7 +8,7 @@
import { useUIStore } from '@/stores/ui' import { useUIStore } from '@/stores/ui'
export default { export default {
name: 'NavbarItemLink', name: 'ControlLink',
props: { props: {
to: { required: true, type: Object } 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> <template>
<a v-if="visible" :disabled="disabled" @click="seek"> <a v-if="visible" :disabled="disabled" @click="seek">
<mdicon <mdicon
class="icon"
name="rewind-10" name="rewind-10"
:size="icon_size"
:title="$t('player.button.seek-backward')" :title="$t('player.button.seek-backward')"
/> />
</a> </a>
@ -14,10 +14,9 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'PlayerButtonSeekBack', name: 'ControlPlayerBack',
props: { props: {
icon_size: { default: 16, type: Number }, offset: { required: true, type: Number }
seek_ms: { required: true, type: Number }
}, },
setup() { setup() {
@ -52,7 +51,7 @@ export default {
methods: { methods: {
seek() { seek() {
if (!this.disabled) { if (!this.disabled) {
webapi.player_seek(this.seek_ms * -1) webapi.player_seek(this.offset * -1)
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<template> <template>
<a :class="{ 'is-info': is_shuffle }" @click="toggle_shuffle_mode"> <a :class="{ 'is-info': is_shuffle }" @click="toggle">
<mdicon <mdicon
class="icon" class="icon"
:name="icon_name" :name="icon"
:size="icon_size" :size="16"
:title="$t(`player.button.${icon_name}`)" :title="$t(`player.button.${icon}`)"
/> />
</a> </a>
</template> </template>
@ -14,20 +14,14 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'PlayerButtonShuffle', name: 'ControlPlayerShuffle',
props: {
icon_size: { default: 16, type: Number }
},
setup() { setup() {
return { return {
playerStore: usePlayerStore() playerStore: usePlayerStore()
} }
}, },
computed: { computed: {
icon_name() { icon() {
if (this.is_shuffle) { if (this.is_shuffle) {
return 'shuffle' return 'shuffle'
} }
@ -37,13 +31,10 @@ export default {
return this.playerStore.shuffle return this.playerStore.shuffle
} }
}, },
methods: { methods: {
toggle_shuffle_mode() { toggle() {
webapi.player_shuffle(!this.is_shuffle) webapi.player_shuffle(!this.is_shuffle)
} }
} }
} }
</script> </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> </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> <template>
<section> <section>
<nav class="buttons is-centered mb-4 fd-is-square"> <nav class="buttons is-centered mb-4">
<router-link <router-link
v-for="index in indices" v-for="index in indices"
:key="index" :key="index"
class="button is-small" class="button is-small is-square"
:to="{ hash: `#index_${index}`, query: $route.query }" :to="{ hash: `#index_${index}`, query: $route.query }"
> >
{{ index }} {{ index }}
@ -20,4 +20,11 @@ export default {
} }
</script> </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>
<template v-for="item in items" :key="item.itemId"> <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 <span
:id="`index_${item.index}`" :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" v-text="item.index"
/> />
</div> </div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div <div
v-if="settingsStore.show_cover_artwork_in_album_lists" v-else
class="media-left" class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
> >
<cover-artwork <cover-artwork
v-if="settingsStore.show_cover_artwork_in_album_lists"
:url="item.item.artwork_url" :url="item.item.artwork_url"
:artist="item.item.artist" :artist="item.item.artist"
:album="item.item.name" :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">
<div class="media-content is-clickable is-clipped"> <div class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
<div> <div
<h1 class="title is-6" v-text="item.item.name" /> class="is-size-7 has-text-grey has-text-weight-bold"
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
v-text="item.item.artist" v-text="item.item.artist"
/> />
<h2 <div
v-if="item.item.date_released && item.item.media_kind === 'music'" 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)" v-text="$filters.date(item.item.date_released)"
/> />
</div> </div>
</div>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)"> <a @click.prevent.stop="open_dialog(item.item)">
<mdicon class="icon has-text-dark" name="dots-vertical" size="16" /> <mdicon class="icon has-text-dark" name="dots-vertical" size="16" />
@ -49,7 +47,7 @@
@remove-podcast="open_remove_podcast_dialog()" @remove-podcast="open_remove_podcast_dialog()"
@play-count-changed="play_count_changed()" @play-count-changed="play_count_changed()"
/> />
<modal-dialog <modal-dialog-action
:close_action="$t('page.podcast.cancel')" :close_action="$t('page.podcast.cancel')"
:delete_action="$t('page.podcast.remove')" :delete_action="$t('page.podcast.remove')"
:show="show_remove_podcast_modal" :show="show_remove_podcast_modal"
@ -64,20 +62,20 @@
<b v-text="rss_playlist_to_remove.name" />) <b v-text="rss_playlist_to_remove.name" />)
</p> </p>
</template> </template>
</modal-dialog> </modal-dialog-action>
</teleport> </teleport>
</template> </template>
<script> <script>
import CoverArtwork from '@/components/CoverArtwork.vue' 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 ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ListAlbums', name: 'ListAlbums',
components: { CoverArtwork, ModalDialog, ModalDialogAlbum }, components: { CoverArtwork, ModalDialogAction, ModalDialogAlbum },
props: { props: {
items: { required: true, type: Object }, items: { required: true, type: Object },
media_kind: { default: '', type: String } media_kind: { default: '', type: String }
@ -148,5 +146,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,17 +1,21 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <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">
<div class="media-content is-clipped"> <div class="media-content">
<span <span
:id="`index_${item.index}`" :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" v-text="item.index"
/> />
</div> </div>
</div> </div>
<div v-else class="media is-align-items-center" @click="open(item.item)"> <div
<div class="media-content is-clickable is-clipped"> v-else
<h1 class="title is-6" v-text="item.item.name" /> 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>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)"> <a @click.prevent.stop="open_dialog(item.item)">
@ -60,5 +64,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,17 +1,21 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <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">
<div class="media-content is-clipped"> <div class="media-content">
<span <span
:id="`index_${item.index}`" :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" v-text="item.index"
/> />
</div> </div>
</div> </div>
<div v-else class="media is-align-items-center" @click="open(item.item)"> <div
<div class="media-content is-clickable is-clipped"> v-else
<h1 class="title is-6" v-text="item.item.name" /> 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>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)"> <a @click.prevent.stop="open_dialog(item.item)">
@ -62,5 +66,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,17 +1,21 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <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">
<div class="media-content is-clipped"> <div class="media-content">
<span <span
:id="`index_${item.index}`" :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" v-text="item.index"
/> />
</div> </div>
</div> </div>
<div v-else class="media is-align-items-center" @click="open(item.item)"> <div
<div class="media-content is-clickable is-clipped"> v-else
<h1 class="title is-6" v-text="item.item.name" /> 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>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)"> <a @click.prevent.stop="open_dialog(item.item)">
@ -63,5 +67,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,39 +1,36 @@
<template> <template>
<transition name="fade"> <modal-dialog :show="show" @close="$emit('close')">
<div v-if="show" class="modal is-active"> <template #content>
<div class="modal-background" @click="$emit('close')" /> <div class="title is-4">
<div class="modal-content"> <a @click="open" v-text="item.name" />
<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>
</div> </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> </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"> <a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" /> <mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.add')" /> <span class="is-size-7" v-text="$t('dialog.artist.add')" />
@ -46,23 +43,17 @@
<mdicon class="icon" name="play" size="16" /> <mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.play')" /> <span class="is-size-7" v-text="$t('dialog.artist.play')" />
</a> </a>
</footer> </template>
</div> </modal-dialog>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template> </template>
<script> <script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogArtist', name: 'ModalDialogArtist',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -89,5 +80,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,118 +1,26 @@
<template> <template>
<nav <nav
class="navbar is-block is-white is-fixed-bottom fd-bottom-navbar" class="navbar is-fixed-bottom"
:class="{ :class="{ 'is-dark': !is_now_playing_page }"
'is-transparent': is_now_playing_page,
'is-dark': !is_now_playing_page
}"
role="navigation"
aria-label="player controls"
> >
<!-- 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"> <div class="navbar-brand is-flex-grow-1">
<navbar-item-link :to="{ name: 'queue' }" class="mr-auto"> <control-link class="navbar-item" :to="{ name: 'queue' }">
<mdicon class="icon" name="playlist-play" size="24" /> <mdicon class="icon" name="playlist-play" />
</navbar-item-link> </control-link>
<navbar-item-link <template v-if="is_now_playing_page">
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' }" :to="{ name: 'now-playing' }"
exact 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" /> <strong v-text="current.title" />
<br /> <br />
<span v-text="current.artist" /> <span v-text="current.artist" />
@ -121,130 +29,40 @@
v-text="$t('navigation.now-playing', { album: current.album })" v-text="$t('navigation.now-playing', { album: current.album })"
/> />
</div> </div>
</navbar-item-link> </control-link>
<player-button-previous <control-player-play class="navbar-item" show_disabled_message />
v-if="is_now_playing_page" </template>
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"
/>
<a <a
class="navbar-item ml-auto" class="navbar-item"
@click="show_player_menu = !show_player_menu" @click="uiStore.show_player_menu = !uiStore.show_player_menu"
> >
<mdicon <mdicon
class="icon" class="icon"
:name="show_player_menu ? 'chevron-down' : 'chevron-up'" :name="uiStore.show_player_menu ? 'chevron-down' : 'chevron-up'"
/> />
</a> </a>
</div>
<!-- Player menu for mobile and tablet -->
<div <div
class="navbar-menu is-hidden-desktop" class="dropdown is-up is-right"
:class="{ 'is-active': show_player_menu }" :class="{ 'is-active': uiStore.show_player_menu }"
> >
<div class="navbar-item"> <div class="dropdown-menu">
<div class="buttons has-addons is-centered"> <div class="dropdown-content">
<player-button-repeat class="button" /> <div class="dropdown-item pt-0">
<player-button-shuffle class="button" /> <control-main-volume />
<player-button-consume class="button" /> <control-output-volume
<player-button-lyrics class="button" /> v-for="output in outputsStore.outputs"
</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"
:key="output.id" :key="output.id"
:output="output" :output="output"
/> />
<!-- Outputs: stream volume --> <control-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>
</div> </div>
<div class="level-item"> <hr class="dropdown-divider" />
<div class="is-flex-grow-1"> <div class="dropdown-item is-flex is-justify-content-center">
<div <div class="buttons has-addons">
class="is-flex is-align-content-center" <control-player-repeat class="button" />
:class="{ 'has-text-grey-light': !playing }" <control-player-shuffle class="button" />
> <control-player-consume class="button" />
<p class="heading" v-text="$t('navigation.stream')" /> <control-player-lyrics class="button" />
<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> </div>
@ -255,168 +73,66 @@
</template> </template>
<script> <script>
import ControlSlider from '@/components/ControlSlider.vue' import ControlLink from '@/components/ControlLink.vue'
import NavbarItemLink from '@/components/NavbarItemLink.vue' import ControlMainVolume from '@/components/ControlMainVolume.vue'
import NavbarItemOutput from '@/components/NavbarItemOutput.vue' import ControlOutputVolume from '@/components/ControlOutputVolume.vue'
import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue' import ControlPlayerBack from '@/components/ControlPlayerBack.vue'
import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue' import ControlPlayerConsume from '@/components/ControlPlayerConsume.vue'
import PlayerButtonNext from '@/components/PlayerButtonNext.vue' import ControlPlayerForward from '@/components/ControlPlayerForward.vue'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue' import ControlPlayerLyrics from '@/components/ControlPlayerLyrics.vue'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue' import ControlPlayerNext from '@/components/ControlPlayerNext.vue'
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat.vue' import ControlPlayerPlay from '@/components/ControlPlayerPlay.vue'
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack.vue' import ControlPlayerPrevious from '@/components/ControlPlayerPrevious.vue'
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward.vue' import ControlPlayerRepeat from '@/components/ControlPlayerRepeat.vue'
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle.vue' import ControlPlayerShuffle from '@/components/ControlPlayerShuffle.vue'
import audio from '@/lib/Audio' import ControlStreamVolume from '@/components/ControlStreamVolume.vue'
import { mdiCancel } from '@mdi/js'
import { useNotificationsStore } from '@/stores/notifications' import { useNotificationsStore } from '@/stores/notifications'
import { useOutputsStore } from '@/stores/outputs' import { useOutputsStore } from '@/stores/outputs'
import { usePlayerStore } from '@/stores/player'
import { useQueueStore } from '@/stores/queue' import { useQueueStore } from '@/stores/queue'
import { useUIStore } from '@/stores/ui' import { useUIStore } from '@/stores/ui'
import webapi from '@/webapi'
export default { export default {
name: 'NavbarBottom', name: 'NavbarBottom',
components: { components: {
ControlSlider, ControlLink,
NavbarItemLink, ControlOutputVolume,
NavbarItemOutput, ControlMainVolume,
PlayerButtonConsume, ControlPlayerBack,
PlayerButtonLyrics, ControlPlayerConsume,
PlayerButtonNext, ControlPlayerForward,
PlayerButtonPlayPause, ControlPlayerLyrics,
PlayerButtonPrevious, ControlPlayerNext,
PlayerButtonRepeat, ControlPlayerPlay,
PlayerButtonSeekBack, ControlPlayerPrevious,
PlayerButtonSeekForward, ControlPlayerRepeat,
PlayerButtonShuffle ControlPlayerShuffle,
ControlStreamVolume
}, },
setup() { setup() {
return { return {
notificationsStore: useNotificationsStore(), notificationsStore: useNotificationsStore(),
outputsStore: useOutputsStore(), outputsStore: useOutputsStore(),
playerStore: usePlayerStore(),
queueStore: useQueueStore(), queueStore: useQueueStore(),
uiStore: useUIStore() 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: { computed: {
is_now_playing_page() { is_now_playing_page() {
return this.$route.name === 'now-playing' return this.$route.name === 'now-playing'
}, },
current() { current() {
return this.queueStore.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> </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> <template>
<nav <nav class="navbar is-light is-fixed-top" :style="zindex">
class="navbar is-light is-fixed-top" <div class="navbar-brand is-flex-grow-1">
:style="zindex" <control-link
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<navbar-item-link
v-if="settingsStore.show_menu_item_playlists" v-if="settingsStore.show_menu_item_playlists"
class="navbar-item"
:to="{ name: 'playlists' }" :to="{ name: 'playlists' }"
> >
<mdicon class="icon" name="music-box-multiple" size="16" /> <mdicon class="icon" name="music-box-multiple" size="16" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="settingsStore.show_menu_item_music" v-if="settingsStore.show_menu_item_music"
class="navbar-item"
:to="{ name: 'music' }" :to="{ name: 'music' }"
> >
<mdicon class="icon" name="music" size="16" /> <mdicon class="icon" name="music" size="16" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="settingsStore.show_menu_item_podcasts" v-if="settingsStore.show_menu_item_podcasts"
class="navbar-item"
:to="{ name: 'podcasts' }" :to="{ name: 'podcasts' }"
> >
<mdicon class="icon" name="microphone" size="16" /> <mdicon class="icon" name="microphone" size="16" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="settingsStore.show_menu_item_audiobooks" v-if="settingsStore.show_menu_item_audiobooks"
class="navbar-item"
:to="{ name: 'audiobooks' }" :to="{ name: 'audiobooks' }"
> >
<mdicon class="icon" name="book-open-variant" size="16" /> <mdicon class="icon" name="book-open-variant" size="16" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="settingsStore.show_menu_item_radio" v-if="settingsStore.show_menu_item_radio"
class="navbar-item"
:to="{ name: 'radio' }" :to="{ name: 'radio' }"
> >
<mdicon class="icon" name="radio" size="16" /> <mdicon class="icon" name="radio" size="16" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="settingsStore.show_menu_item_files" v-if="settingsStore.show_menu_item_files"
class="navbar-item"
:to="{ name: 'files' }" :to="{ name: 'files' }"
> >
<mdicon class="icon" name="folder-open" size="16" /> <mdicon class="icon" name="folder-open" size="16" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="settingsStore.show_menu_item_search" v-if="settingsStore.show_menu_item_search"
class="navbar-item"
:to="{ name: searchStore.search_source }" :to="{ name: searchStore.search_source }"
> >
<mdicon class="icon" name="magnify" size="16" /> <mdicon class="icon" name="magnify" size="16" />
</navbar-item-link> </control-link>
<div <a
class="navbar-burger" class="navbar-item ml-auto"
:class="{ 'is-active': show_burger_menu }" @click="uiStore.show_burger_menu = !uiStore.show_burger_menu"
@click="show_burger_menu = !show_burger_menu"
> >
<span /> <mdicon
<span /> class="icon"
<span /> :name="uiStore.show_burger_menu ? 'close' : 'menu'"
</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" />
</a> </a>
<div class="navbar-dropdown is-right"> <div
<navbar-item-link :to="{ name: 'playlists' }"> 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" /> <mdicon class="icon" name="music-box-multiple" size="16" />
</span>
<b v-text="$t('navigation.playlists')" /> <b v-text="$t('navigation.playlists')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'music' }"> <control-link class="dropdown-item" :to="{ name: 'music' }">
<span class="icon-text">
<mdicon class="icon" name="music" size="16" /> <mdicon class="icon" name="music" size="16" />
</span>
<b v-text="$t('navigation.music')" /> <b v-text="$t('navigation.music')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'music-artists' }"> <control-link class="dropdown-item" :to="{ name: 'music-artists' }">
<span class="pl-5" v-text="$t('navigation.artists')" /> <span class="pl-5" v-text="$t('navigation.artists')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'music-albums' }"> <control-link class="dropdown-item" :to="{ name: 'music-albums' }">
<span class="pl-5" v-text="$t('navigation.albums')" /> <span class="pl-5" v-text="$t('navigation.albums')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'music-genres' }"> <control-link class="dropdown-item" :to="{ name: 'music-genres' }">
<span class="pl-5" v-text="$t('navigation.genres')" /> <span class="pl-5" v-text="$t('navigation.genres')" />
</navbar-item-link> </control-link>
<navbar-item-link <control-link
v-if="spotify_enabled" v-if="spotify_enabled"
class="dropdown-item"
:to="{ name: 'music-spotify' }" :to="{ name: 'music-spotify' }"
> >
<span class="pl-5" v-text="$t('navigation.spotify')" /> <span class="pl-5" v-text="$t('navigation.spotify')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'podcasts' }"> <control-link class="dropdown-item" :to="{ name: 'podcasts' }">
<span class="icon-text">
<mdicon class="icon" name="microphone" size="16" /> <mdicon class="icon" name="microphone" size="16" />
</span>
<b v-text="$t('navigation.podcasts')" /> <b v-text="$t('navigation.podcasts')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'audiobooks' }"> <control-link class="dropdown-item" :to="{ name: 'audiobooks' }">
<span class="icon-text">
<mdicon class="icon" name="book-open-variant" size="16" /> <mdicon class="icon" name="book-open-variant" size="16" />
</span>
<b v-text="$t('navigation.audiobooks')" /> <b v-text="$t('navigation.audiobooks')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'radio' }"> <control-link class="dropdown-item" :to="{ name: 'radio' }">
<span class="icon-text">
<mdicon class="icon" name="radio" size="16" /> <mdicon class="icon" name="radio" size="16" />
</span>
<b v-text="$t('navigation.radio')" /> <b v-text="$t('navigation.radio')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: 'files' }"> <control-link class="dropdown-item" :to="{ name: 'files' }">
<span class="icon-text">
<mdicon class="icon" name="folder-open" size="16" /> <mdicon class="icon" name="folder-open" size="16" />
</span>
<b v-text="$t('navigation.files')" /> <b v-text="$t('navigation.files')" />
</navbar-item-link> </control-link>
<navbar-item-link :to="{ name: searchStore.search_source }"> <control-link
class="dropdown-item"
:to="{ name: searchStore.search_source }"
>
<span class="icon-text">
<mdicon class="icon" name="magnify" size="16" /> <mdicon class="icon" name="magnify" size="16" />
</span>
<b v-text="$t('navigation.search')" /> <b v-text="$t('navigation.search')" />
</navbar-item-link> </control-link>
<hr class="my-3" /> <hr class="my-3" />
<navbar-item-link :to="{ name: 'settings-webinterface' }"> <control-link
class="dropdown-item"
:to="{ name: 'settings-webinterface' }"
>
{{ $t('navigation.settings') }} {{ $t('navigation.settings') }}
</navbar-item-link> </control-link>
<a <a
class="navbar-item" class="dropdown-item"
@click.stop.prevent="open_update_dialog()" @click.stop.prevent="open_update_dialog()"
v-text="$t('navigation.update-library')" v-text="$t('navigation.update-library')"
/> />
<navbar-item-link :to="{ name: 'about' }"> <control-link class="dropdown-item" :to="{ name: 'about' }">
{{ $t('navigation.about') }} {{ $t('navigation.about') }}
</navbar-item-link> </control-link>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-show="show_settings_menu" v-show="show_settings_menu"
class="is-overlay" class="is-overlay"
@ -139,7 +155,7 @@
</template> </template>
<script> <script>
import NavbarItemLink from '@/components/NavbarItemLink.vue' import ControlLink from '@/components/ControlLink.vue'
import { useSearchStore } from '@/stores/search' import { useSearchStore } from '@/stores/search'
import { useServicesStore } from '@/stores/services' import { useServicesStore } from '@/stores/services'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
@ -147,7 +163,7 @@ import { useUIStore } from '@/stores/ui'
export default { export default {
name: 'NavbarTop', name: 'NavbarTop',
components: { NavbarItemLink }, components: { ControlLink },
setup() { setup() {
return { return {
@ -165,22 +181,6 @@ export default {
}, },
computed: { 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() { spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid return this.servicesStore.spotify.webapi_token_valid
}, },
@ -203,12 +203,10 @@ export default {
this.show_settings_menu = !this.show_settings_menu this.show_settings_menu = !this.show_settings_menu
}, },
open_update_dialog() { open_update_dialog() {
this.show_update_dialog = true this.uiStore.show_update_dialog = true
this.show_settings_menu = false this.show_settings_menu = false
this.show_burger_menu = false this.uiStore.show_burger_menu = false
} }
} }
} }
</script> </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> <template>
<section class="section fd-tabs-section"> <section class="section tabs-section">
<div class="container"> <div class="container">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-four-fifths"> <div class="column is-four-fifths">
@ -67,5 +67,3 @@ export default {
name: 'TabsAudiobooks' name: 'TabsAudiobooks'
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ export const filters = {
.toLocaleString(DateTime.DATETIME_MED) .toLocaleString(DateTime.DATETIME_MED)
}, },
durationInDays(value) { durationInDays(value) {
const minutes = Math.floor(value / 60000) const minutes = Math.floor(value / 60)
if (minutes > 1440) { if (minutes > 1440) {
return Duration.fromObject({ minutes }) return Duration.fromObject({ minutes })
.shiftTo('days', 'hours', 'minutes') .shiftTo('days', 'hours', 'minutes')
@ -53,6 +53,6 @@ export const filters = {
}, },
timeFromNow(value) { timeFromNow(value) {
const diff = DateTime.now().diff(DateTime.fromISO(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}.", "compiled-with": "Compiliert mit Unterstützung von {options}.",
"library": "Bibliothek", "library": "Bibliothek",
"more": "mehr", "more": "mehr",
"name": "Name",
"total-playtime": "Gesamte Spielzeit", "total-playtime": "Gesamte Spielzeit",
"tracks": "Tracks", "tracks": "Tracks",
"update": "Neu einlesen", "update": "Neu einlesen",
@ -407,8 +408,8 @@
"count": "{count} Track|{count} Track|{count} Tracks", "count": "{count} Track|{count} Track|{count} Tracks",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"hide-previous": "Vorherige verbergen", "hide-previous": "Vorherige verbergen",
"title": "Warteschlange", "save": "Speichern",
"save": "Speichern" "title": "Warteschlange"
}, },
"radio": { "radio": {
"count": "{count} Station|{count} Station|{count} Stationen", "count": "{count} Station|{count} Station|{count} Stationen",
@ -445,6 +446,7 @@
"discogs": "Discogs", "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-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:", "explanation-2": "Zusätzlich kann auf folgende Artwork-Anbieter zugegriffen werden:",
"show-coverart": "Zeige Cover-Artwork in der Albumliste",
"spotify": "Spotify", "spotify": "Spotify",
"streaming": "Bereitgestellte Artwork von Radiostationen ignorieren" "streaming": "Bereitgestellte Artwork von Radiostationen ignorieren"
}, },
@ -460,7 +462,6 @@
"verify": "Verifizieren" "verify": "Verifizieren"
}, },
"general": { "general": {
"album-lists": "Album-Listen",
"audiobooks": "Hörbücher", "audiobooks": "Hörbücher",
"files": "Dateien", "files": "Dateien",
"genres": "Genres", "genres": "Genres",
@ -473,17 +474,16 @@
"playlists": "Playlisten", "playlists": "Playlisten",
"podcasts": "Podcasts", "podcasts": "Podcasts",
"radio": "Radio", "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", "recently-added-page": "Kürzlich hinzugefügt-Seite",
"search": "Suche", "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-1": "Komma getrennte Liste der Genres, für die der Komponist angezeigt werden soll.",
"show-composer-genres-info-2": "Leer lassen, um ihn immer anzuzeigen.", "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-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-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-composer": "Komponisten anzeigen",
"show-coverart": "Zeige Cover-Artwork in der Albumliste", "show-path": "Dateipfad anzeigen"
"show-path": "Dateipfad auf der \"Aktuell läuft\"-Seite anzeigen"
}, },
"services": { "services": {
"lastfm": { "lastfm": {
@ -495,8 +495,7 @@
"spotify": { "spotify": {
"no-support": "OwnTone wurde entweder ohne Unterstützung für Spotify erstellt oder libspotify ist nicht installiert.", "no-support": "OwnTone wurde entweder ohne Unterstützung für Spotify erstellt oder libspotify ist nicht installiert.",
"logged-as": "Angemeldet als ", "logged-as": "Angemeldet als ",
"requirements": "Spotify Premium Abo erforderlich.", "requirements": "Spotify Premium Abo erforderlich. Zugriff auf die Spotify Web-Api ermöglicht scannen der Spotify-Blibliothek. Erforderliche scopes sind: ",
"scopes": "Zugriff auf die Spotify Web-Api ermöglicht scannen der Spotify-Blibliothek. Erforderliche scopes sind: ",
"user": "Zugriff gestattet für ", "user": "Zugriff gestattet für ",
"authorize": "Authorisiere Web-API-Zugriff", "authorize": "Authorisiere Web-API-Zugriff",
"grant-access": "Zugriff auf die Spotify Web-API gestatten", "grant-access": "Zugriff auf die Spotify Web-API gestatten",
@ -572,10 +571,6 @@
"toggle-lyrics": "Liedtexte anzeigen/verbergen" "toggle-lyrics": "Liedtexte anzeigen/verbergen"
} }
}, },
"setting": {
"not-saved": " (Fehler beim Speichern der Einstellungen)",
"saved": " (Einstellungen gesichert)"
},
"server": { "server": {
"connection-failed": "Fehler bei Verbindung zum OwnTone-Server", "connection-failed": "Fehler bei Verbindung zum OwnTone-Server",
"request-failed": "Anfrage gescheitert (Status: {status} {cause} {url})", "request-failed": "Anfrage gescheitert (Status: {status} {cause} {url})",

View File

@ -241,6 +241,7 @@
"compiled-with": "Compiled with support for {options}.", "compiled-with": "Compiled with support for {options}.",
"library": "Library", "library": "Library",
"more": "more", "more": "more",
"name": "Name",
"total-playtime": "Total playtime", "total-playtime": "Total playtime",
"tracks": "Tracks", "tracks": "Tracks",
"update": "Update", "update": "Update",
@ -407,8 +408,8 @@
"count": "{count} track|{count} track|{count} tracks", "count": "{count} track|{count} track|{count} tracks",
"edit": "Edit", "edit": "Edit",
"hide-previous": "Hide previous", "hide-previous": "Hide previous",
"title": "Queue", "save": "Save",
"save": "Save" "title": "Queue"
}, },
"radio": { "radio": {
"count": "{count} station|{count} station|{count} stations", "count": "{count} station|{count} station|{count} stations",
@ -473,16 +474,16 @@
"playlists": "Playlists", "playlists": "Playlists",
"podcasts": "Podcasts", "podcasts": "Podcasts",
"radio": "Radio", "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", "recently-added-page": "Recently added page",
"search": "Search", "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-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-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-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-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-composer": "Show composer",
"show-path": "Show filepath on the \"Now playing\" page" "show-path": "Show filepath"
}, },
"services": { "services": {
"lastfm": { "lastfm": {
@ -494,8 +495,7 @@
"spotify": { "spotify": {
"no-support": "OwnTone was either built without support for Spotify or libspotify is not installed.", "no-support": "OwnTone was either built without support for Spotify or libspotify is not installed.",
"logged-as": "Logged in as ", "logged-as": "Logged in as ",
"requirements": "You must have a Spotify premium account.", "requirements": "You must have a Spotify premium account. Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are: ",
"scopes": "Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are: ",
"user": "Access granted for ", "user": "Access granted for ",
"authorize": "Authorize Web API access", "authorize": "Authorize Web API access",
"grant-access": "Grant access to the Spotify Web API", "grant-access": "Grant access to the Spotify Web API",
@ -571,10 +571,6 @@
"toggle-lyrics": "Toggle lyrics" "toggle-lyrics": "Toggle lyrics"
} }
}, },
"setting": {
"not-saved": " (error saving setting)",
"saved": " (setting saved)"
},
"server": { "server": {
"connection-failed": "Failed to connect to OwnTone server", "connection-failed": "Failed to connect to OwnTone server",
"request-failed": "Request failed (status: {status} {cause} {url})", "request-failed": "Request failed (status: {status} {cause} {url})",

View File

@ -241,6 +241,7 @@
"compiled-with": "Compilé avec les options {options}.", "compiled-with": "Compilé avec les options {options}.",
"library": "Bibliothèque", "library": "Bibliothèque",
"more": "plus", "more": "plus",
"name": "Nom",
"total-playtime": "Durée totale de lecture", "total-playtime": "Durée totale de lecture",
"tracks": "Pistes", "tracks": "Pistes",
"update": "Actualiser", "update": "Actualiser",
@ -407,8 +408,8 @@
"count": "{count} piste|{count} piste|{count} pistes", "count": "{count} piste|{count} piste|{count} pistes",
"edit": "Éditer", "edit": "Éditer",
"hide-previous": "Masquer lhistorique", "hide-previous": "Masquer lhistorique",
"queue": "File dattente", "save": "Enregistrer",
"save": "Enregistrer" "title": "File dattente"
}, },
"radio": { "radio": {
"count": "{count} station|{count} station|{count} stations", "count": "{count} station|{count} station|{count} stations",
@ -473,16 +474,16 @@
"playlists": "Listes de lecture", "playlists": "Listes de lecture",
"podcasts": "Podcasts", "podcasts": "Podcasts",
"radio": "Radio", "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 »", "recently-added-page": "Page « Ajouts récents »",
"search": "Recherche", "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-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-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-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-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": { "services": {
"lastfm": { "lastfm": {
@ -494,8 +495,7 @@
"spotify": { "spotify": {
"no-support": "Loption Spotify nest pas présente.", "no-support": "Loption Spotify nest pas présente.",
"logged-as": "Connecté en tant que ", "logged-as": "Connecté en tant que ",
"requirements": "Vous devez posséder un compte Spotify Premium.", "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 :",
"scopes": "Laccès à lAPI de Spotify permet lanalyse de votre bibliothèque Spotify. Les champs dapplication requis sont les suivants :",
"user": "Accès autorisé pour ", "user": "Accès autorisé pour ",
"authorize": "Autoriser laccès à lAPI", "authorize": "Autoriser laccès à lAPI",
"grant-access": "Accordez laccès à lAPI de Spotify", "grant-access": "Accordez laccès à lAPI de Spotify",
@ -571,10 +571,6 @@
"toggle-lyrics": "Voir/Cacher les paroles" "toggle-lyrics": "Voir/Cacher les paroles"
} }
}, },
"setting": {
"not-saved": " (erreur à lenregistrement du réglage)",
"saved": " (réglage enregistré)"
},
"server": { "server": {
"connection-failed": "Échec de connexion au serveur", "connection-failed": "Échec de connexion au serveur",
"request-failed": "La requête a échoué (status: {status} {cause} {url})", "request-failed": "La requête a échoué (status: {status} {cause} {url})",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,15 @@
<div> <div>
<content-with-heading> <content-with-heading>
<template #heading-left> <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>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -19,14 +27,6 @@
</div> </div>
</template> </template>
<template #content> <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" /> <list-albums :items="albums" />
<modal-dialog-artist <modal-dialog-artist
:item="artist" :item="artist"
@ -87,5 +87,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

@ -2,7 +2,21 @@
<div> <div>
<content-with-heading> <content-with-heading>
<template #heading-left> <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>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -19,21 +33,6 @@
</div> </div>
</template> </template>
<template #content> <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" /> <list-albums :items="albums" />
<modal-dialog-composer <modal-dialog-composer
:item="composer" :item="composer"
@ -104,5 +103,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -5,16 +5,35 @@
<index-button-list :indices="tracks.indices" /> <index-button-list :indices="tracks.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <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 <control-dropdown
v-model:value="selected_grouping_id" v-model:value="uiStore.composer_tracks_sort"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <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>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -31,23 +50,6 @@
</div> </div>
</template> </template>
<template #content> <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" /> <list-tracks :items="tracks" :expression="expression" />
<modal-dialog-composer <modal-dialog-composer
:item="composer" :item="composer"
@ -130,17 +132,9 @@ export default {
expression() { expression() {
return `composer is "${this.composer.name}" and media_kind is music` 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() { tracks() {
const { options } = this.groupings.find( 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) return this.tracks_list.group(options)
} }
@ -160,5 +154,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

@ -5,7 +5,17 @@
<index-button-list :indices="albums.indices" /> <index-button-list :indices="albums.indices" />
</template> </template>
<template #heading-left> <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>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -22,17 +32,6 @@
</div> </div>
</template> </template>
<template #content> <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" /> <list-albums :items="albums" />
<modal-dialog-genre <modal-dialog-genre
:item="genre" :item="genre"
@ -109,5 +108,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -5,16 +5,29 @@
<index-button-list :indices="tracks.indices" /> <index-button-list :indices="tracks.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <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 <control-dropdown
v-model:value="selected_grouping_id" v-model:value="uiStore.genre_tracks_sort"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <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>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -31,17 +44,6 @@
</div> </div>
</template> </template>
<template #content> <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" /> <list-tracks :items="tracks" :expression="expression" />
<modal-dialog-genre <modal-dialog-genre
:item="genre" :item="genre"
@ -126,17 +128,9 @@ export default {
expression() { expression() {
return `genre is "${this.genre.name}" and media_kind is ${this.media_kind}` 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() { tracks() {
const { options } = this.groupings.find( 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) return this.tracks_list.group(options)
} }
@ -157,5 +151,3 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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