diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e50c47d5..c8322243 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,53 +1,47 @@ name: 'Close stale issues and PRs' on: - workflow_dispatch: - schedule: - - cron: '30 1 * * *' + workflow_dispatch: + schedule: + - cron: '30 1 * * *' permissions: - contents: read + contents: read jobs: - stale: - permissions: - issues: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v5 - with: - process-only: 'issues, prs' - issue-inactive-days: 120 - pr-inactive-days: 120 - log-output: true - add-issue-labels: 'frozen-due-to-age' - add-pr-labels: 'frozen-due-to-age' - issue-comment: > - This issue has been automatically locked since there - has not been any recent activity after it was closed. - Please open a new issue for related bugs. - pr-comment: > - This pull request has been automatically locked since there - has not been any recent activity after it was closed. - Please open a new issue for related bugs. - - uses: actions/stale@v9 - with: - operations-per-run: 999 - days-before-issue-stale: 180 - days-before-pr-stale: 180 - days-before-issue-close: 30 - days-before-pr-close: 30 - stale-issue-message: > - This issue has been automatically marked as stale because it has not had recent activity. - The resources of the Feishin team are limited, and so we are asking for your help. + stale: + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + process-only: 'issues, prs' + issue-inactive-days: 120 + pr-inactive-days: 120 + log-output: true + add-issue-labels: 'frozen-due-to-age' + add-pr-labels: 'frozen-due-to-age' + - uses: actions/stale@v9 + with: + operations-per-run: 999 + days-before-issue-stale: 180 + days-before-pr-stale: 180 + days-before-issue-close: 30 + days-before-pr-close: 30 + stale-issue-message: > + This issue has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help. - If this is a **bug** and you can still reproduce this error on the development branch, please reply with all of the information you have about it in order to keep the issue open. + If this is a **bug** and you can still reproduce this error on the development branch, please reply with all of the information you have about it in order to keep the issue open. - This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. - stale-pr-message: > - This PR has been automatically marked as stale because it has not had recent activity. - The resources of the Feishin team are limited, and so we are asking for your help. + This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. - This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. - stale-issue-label: 'stale' - exempt-issue-labels: 'enhancement,keep,security' - stale-pr-label: 'stale' - exempt-pr-labels: 'keep,security' + + stale-pr-message: > + This PR has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help. + + This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. + + + stale-issue-label: 'stale' + exempt-issue-labels: 'enhancement,keep,security' + stale-pr-label: 'stale' + exempt-pr-labels: 'keep,security' diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 109685d7..59563487 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -8,7 +8,7 @@ arrowParens: always proseWrap: never htmlWhitespaceSensitivity: strict endOfLine: lf -singleAttributePerLine: true +singleAttributePerLine: false bracketSpacing: true plugins: - prettier-plugin-packagejson diff --git a/README.md b/README.md index 0959ec5d..03f1dbfc 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi - [LMS](https://github.com/epoupon/lms) - [Nextcloud Music](https://apps.nextcloud.com/apps/music) - [Supysonic](https://github.com/spl0k/supysonic) + - [Qm-Music](https://github.com/chenqimiao/qm-music) - More (?) ### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux diff --git a/electron-builder.yml b/electron-builder.yml index 8ac46589..30b464e5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -35,39 +35,13 @@ mac: notarize: false dmg: contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }] -deb: - depends: - - libgssapi_krb5.so.2 - - libavahi-common.so.3 - - libavahi-client.so.3 - - libkrb5.so.3 - - libkrb5support.so.0 - - libkeyutils.so.1 - - libcups.so.2 -rpm: - depends: - - libgssapi_krb5.so.2 - - libavahi-common.so.3 - - libavahi-client.so.3 - - libkrb5.so.3 - - libkrb5support.so.0 - - libkeyutils.so.1 - - libcups.so.2 -freebsd: - depends: - - libgssapi_krb5.so.2 - - libavahi-common.so.3 - - libavahi-client.so.3 - - libkrb5.so.3 - - libkrb5support.so.0 - - libkeyutils.so.1 - - libcups.so.2 linux: target: - AppImage - tar.xz category: AudioVideo;Audio;Player icon: assets/icons/icon.png + artifactName: ${productName}-${os}-${arch}.${ext} npmRebuild: false publish: provider: github diff --git a/package.json b/package.json index 900e8f53..d54e2d96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "feishin", - "version": "0.17.0", + "version": "0.19.0", "description": "A modern self-hosted music player.", "keywords": [ "subsonic", @@ -75,7 +75,7 @@ "@tanstack/react-query-devtools": "^4.32.1", "@tanstack/react-query-persist-client": "^4.32.1", "@ts-rest/core": "^3.23.0", - "@xhayper/discord-rpc": "^1.0.24", + "@xhayper/discord-rpc": "^1.3.0", "audiomotion-analyzer": "^4.5.0", "auto-text-size": "^0.2.3", "axios": "^1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b01e31ba..5fff7689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^3.23.0 version: 3.52.1(@types/node@22.15.32)(zod@3.25.23) '@xhayper/discord-rpc': - specifier: ^1.0.24 - version: 1.2.1 + specifier: ^1.3.0 + version: 1.3.0 audiomotion-analyzer: specifier: ^4.5.0 version: 4.5.0 @@ -509,8 +509,8 @@ packages: resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} engines: {node: '>=18'} - '@discordjs/rest@2.5.0': - resolution: {integrity: sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==} + '@discordjs/rest@2.5.1': + resolution: {integrity: sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw==} engines: {node: '>=18'} '@discordjs/util@1.1.1': @@ -769,6 +769,10 @@ packages: resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -781,8 +785,8 @@ packages: resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.3.4': + resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@floating-ui/core@1.7.0': @@ -837,26 +841,24 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} '@keyv/serialize@1.0.3': resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==} @@ -1359,8 +1361,8 @@ packages: resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - '@xhayper/discord-rpc@1.2.1': - resolution: {integrity: sha512-Ch04/7hq0nfV47nJzDcLIKx0SLUcPOMlkYV43faWpKtEO9SgLrTD4FAOMBBT+JORceQytnzBMPvktW2q9ZCMiw==} + '@xhayper/discord-rpc@1.3.0': + resolution: {integrity: sha512-0NmUTiODl7u3UEjmO6y0Syp3dmgVLAt2EHrH4QKTQcXRwtF8Wl7Eipdn/GSSZ8HkDwxQFvcDGJMxT9VWB0pH8g==} engines: {node: '>=18.20.7'} '@xmldom/xmldom@0.8.10': @@ -1960,11 +1962,8 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - discord-api-types@0.37.120: - resolution: {integrity: sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==} - - discord-api-types@0.38.8: - resolution: {integrity: sha512-xuRXPD44FcbKHrQK15FS1HFlMRNJtsaZou/SVws18vQ7zHqmlxyDktMkZpyvD6gE2ctGOVYC/jUyoMMAyBWfcw==} + discord-api-types@0.38.18: + resolution: {integrity: sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ==} dmg-builder@26.0.12: resolution: {integrity: sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==} @@ -2373,8 +2372,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} format-duration@2.0.0: @@ -4140,8 +4139,8 @@ packages: resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} engines: {node: '>= 10'} - socks@2.8.5: - resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==} + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} sort-keys@5.1.0: @@ -4461,10 +4460,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@6.21.1: - resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} - engines: {node: '>=18.17'} - undici@6.21.3: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} @@ -4708,6 +4703,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml2js@0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} engines: {node: '>=4.0.0'} @@ -4798,8 +4805,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1': dependencies: @@ -4855,8 +4862,8 @@ snapshots: dependencies: '@babel/parser': 7.27.2 '@babel/types': 7.27.1 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': @@ -4964,17 +4971,17 @@ snapshots: '@discordjs/collection@2.1.1': {} - '@discordjs/rest@2.5.0': + '@discordjs/rest@2.5.1': dependencies: '@discordjs/collection': 2.1.1 '@discordjs/util': 1.1.1 '@sapphire/async-queue': 1.5.5 '@sapphire/snowflake': 3.5.5 '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.38.8 + discord-api-types: 0.38.18 magic-bytes.js: 1.12.1 tslib: 2.8.1 - undici: 6.21.1 + undici: 6.21.3 '@discordjs/util@1.1.1': {} @@ -5218,6 +5225,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -5236,9 +5247,9 @@ snapshots: '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.3.4': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.1 levn: 0.4.1 '@floating-ui/core@1.7.0': @@ -5294,28 +5305,27 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.10': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 optional: true '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 '@keyv/serialize@1.0.3': dependencies: @@ -5856,12 +5866,12 @@ snapshots: '@vladfrangu/async_event_emitter@2.4.6': {} - '@xhayper/discord-rpc@1.2.1': + '@xhayper/discord-rpc@1.3.0': dependencies: - '@discordjs/rest': 2.5.0 + '@discordjs/rest': 2.5.1 '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.120 - ws: 8.18.2 + discord-api-types: 0.38.18 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6074,7 +6084,7 @@ snapshots: axios@1.9.0: dependencies: follow-redirects: 1.15.9 - form-data: 4.0.2 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -6572,9 +6582,7 @@ snapshots: dependencies: path-type: 4.0.0 - discord-api-types@0.37.120: {} - - discord-api-types@0.38.8: {} + discord-api-types@0.38.18: {} dmg-builder@26.0.12(electron-builder-squirrel-windows@26.0.12): dependencies: @@ -6720,7 +6728,7 @@ snapshots: builder-util: 26.0.11 builder-util-runtime: 9.3.1 chalk: 4.1.2 - form-data: 4.0.2 + form-data: 4.0.4 fs-extra: 10.1.0 lazy-val: 1.0.5 mime: 2.6.0 @@ -7021,7 +7029,7 @@ snapshots: '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.27.0 - '@eslint/plugin-kit': 0.3.1 + '@eslint/plugin-kit': 0.3.4 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -7183,11 +7191,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.2: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 format-duration@2.0.0: {} @@ -8958,11 +8967,11 @@ snapshots: dependencies: agent-base: 6.0.2 debug: 4.4.1 - socks: 2.8.5 + socks: 2.8.6 transitivePeerDependencies: - supports-color - socks@2.8.5: + socks@2.8.6: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 @@ -9262,7 +9271,7 @@ snapshots: terser@5.39.2: dependencies: - '@jridgewell/source-map': 0.3.6 + '@jridgewell/source-map': 0.3.10 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -9386,8 +9395,6 @@ snapshots: undici-types@6.21.0: {} - undici@6.21.1: {} - undici@6.21.3: {} unique-filename@2.0.1: @@ -9633,6 +9640,8 @@ snapshots: ws@8.18.2: {} + ws@8.18.3: {} + xml2js@0.4.23: dependencies: sax: 1.4.1 diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 0656d426..651efabd 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -19,6 +19,7 @@ import nl from './locales/nl.json'; import pl from './locales/pl.json'; import ptBr from './locales/pt-BR.json'; import ru from './locales/ru.json'; +import sl from './locales/sl.json'; import sr from './locales/sr.json'; import sv from './locales/sv.json'; import ta from './locales/ta.json'; @@ -43,6 +44,7 @@ const resources = { pl: { translation: pl }, 'pt-BR': { translation: ptBr }, ru: { translation: ru }, + sl: { translation: sl }, sr: { translation: sr }, sv: { translation: sv }, ta: { translation: ta }, @@ -119,6 +121,10 @@ export const languages = [ label: 'Русский', value: 'ru', }, + { + label: 'Slovenščina', + value: 'sl', + }, { label: 'Srpski', value: 'sr', diff --git a/src/i18n/locales/cs.json b/src/i18n/locales/cs.json index fb5d07eb..9892ab60 100644 --- a/src/i18n/locales/cs.json +++ b/src/i18n/locales/cs.json @@ -271,7 +271,9 @@ "discordPausedStatus": "zobrazit rich presence při pozastavení", "discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav", "preservePitch": "zachovat výšku", - "preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání" + "preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání", + "notify": "povolit oznámení o skladbách", + "notify_description": "zobrazit oznámení při změně aktuální skladby" }, "action": { "editPlaylist": "upravit $t(entity.playlist_one)", @@ -393,7 +395,9 @@ "additionalParticipants": "další přispívající", "tags": "štítky", "viewReleaseNotes": "zobrazit seznam změn", - "newVersion": "byla nainstalována nová verze ({{version}})" + "newVersion": "byla nainstalována nová verze ({{version}})", + "bitDepth": "bitová hloubka", + "sampleRate": "vzorkovací frekvence" }, "table": { "config": { @@ -495,7 +499,8 @@ "badAlbum": "tuto stránku vidíte, protože tato skladba není součástí alba. tento problém může nastat, pokud máte skladbu na nejvyšší úrovni vaší složky s hudbou. jellyfin seskupuje skladby pouze, pokud se nacházejí ve složce.", "networkError": "vyskytla se chyba sítě", "openError": "nepodařilo se otevřít soubor", - "badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje" + "badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje", + "notificationDenied": "oprávnění k posílání oznámení byla zamítnuta. toto nastavení nemá žádný vliv" }, "filter": { "mostPlayed": "nejvíce přehráváno", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a8caec4b..8693bf5a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -36,6 +36,7 @@ "ascending": "ascending", "backward": "backward", "biography": "biography", + "bitDepth": "bit depth", "bitrate": "bitrate", "bpm": "bpm", "cancel": "cancel", @@ -99,6 +100,7 @@ "resetToDefault": "reset to default", "restartRequired": "restart required", "right": "right", + "sampleRate": "sample rate", "save": "save", "saveAndReplace": "save and replace", "saveAs": "save as", @@ -286,6 +288,11 @@ "updateServer": { "success": "server updated successfully", "title": "update server" + }, + "privateMode": { + "enabled": "private mode enabled, playback status is now hidden from external integrations", + "disabled": "private mode disabled, playback status is now visible to enabled external integrations", + "title": "private mode" } }, "page": { @@ -319,6 +326,8 @@ "goBack": "go back", "goForward": "go forward", "manageServers": "manage servers", + "privateModeOff": "turn off private mode", + "privateModeOn": "turn on private mode", "openBrowserDevtools": "open browser devtools", "quit": "$t(common.quit)", "selectServer": "select server", @@ -527,6 +536,10 @@ "discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome", "discordUpdateInterval": "{{discord}} rich presence update interval", "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", + "discordDisplayType": "{{discord}} presence display type", + "discordDisplayType_description": "changes what you are listening to in your status", + "discordDisplayType_songname": "song name", + "discordDisplayType_artistname": "artist name(s)", "doubleClickBehavior": "queue all searched tracks when double clicking", "doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued", "enableRemote": "enable remote control server", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index ca959042..f3c1e51c 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -271,7 +271,9 @@ "discordPausedStatus": "Mostrar estado de actividad cuando esté en pausa", "discordPausedStatus_description": "Cuando está activado, el estado mostrará cuando el reproductor esté en pausa", "preservePitch": "Mantener el tono", - "preservePitch_description": "Mantiene el tono cuando se modifica la velocidad de reproducción" + "preservePitch_description": "Mantiene el tono cuando se modifica la velocidad de reproducción", + "notify": "Activar notificaciones de canciones", + "notify_description": "Muestra notificaciones cuando se cambia la canción actual" }, "action": { "editPlaylist": "editar $t(entity.playlist_one)", @@ -393,7 +395,9 @@ "additionalParticipants": "Participantes adicionales", "tags": "Etiquetas", "newVersion": "Una nueva versión ha sido instalada ({{version}})", - "viewReleaseNotes": "Ver notas de lanzamiento" + "viewReleaseNotes": "Ver notas de lanzamiento", + "bitDepth": "Profundidad de bit", + "sampleRate": "Frecuencia de muestreo" }, "error": { "remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto", @@ -418,7 +422,8 @@ "badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tienes una canción en el nivel superior de tu carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.", "networkError": "Ocurrió un error de red", "openError": "No se pudo abrir el archivo", - "badValue": "Opción inválida \"{{value}}\". Este valor ya no existe" + "badValue": "Opción inválida \"{{value}}\". Este valor ya no existe", + "notificationDenied": "Se denegaron los permisos para notificaciones. Esta configuración no tiene efecto" }, "filter": { "mostPlayed": "más reproducido", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 0feb4e0e..dbb7301d 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -101,6 +101,7 @@ "forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer", "setting": "paramètre", "setting_one": "paramètre", + "setting_many": "", "setting_other": "paramètres", "version": "version", "title": "titre", @@ -154,7 +155,9 @@ "additionalParticipants": "participants additionnels", "tags": "tags", "newVersion": "une nouvelle version vient d'être installé ({{version}})", - "viewReleaseNotes": "voir la note de version" + "viewReleaseNotes": "voir la note de version", + "sampleRate": "taux d'échantillonnage", + "bitDepth": "bit par échantillon" }, "error": { "remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port", @@ -179,7 +182,8 @@ "openError": "impossible d'ouvrir le fichier", "networkError": "une erreur de réseau est survenue", "badAlbum": "vous voyez cette page parce que cette chanson ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez une chanson qui n'est pas dans votre répertoire de musique. jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\".", - "badValue": "option {{value}} invalide. Cette valeur n'existe plus" + "badValue": "option {{value}} invalide. Cette valeur n'existe plus", + "notificationDenied": "les autorisations pour les notifications ont été refusées. ce paramètre n'a aucun effet" }, "filter": { "mostPlayed": "plus joués", @@ -400,7 +404,7 @@ "discordIdleStatus_description": "quand activé, mettre à jour le status pendant que le lecteur est inactif", "showSkipButtons": "affiche les boutons suivants et précédents", "minimumScrobblePercentage": "durée minimal du scobble (pourcentage)", - "lyricFetch": "récupère les paroles depuis internet", + "lyricFetch": "récupérer les paroles depuis internet", "scrobble": "scrobble", "enableRemote_description": "activer le serveur de contrôle à distance, qui permet à d'autres appareils de contrôler l'application", "fontType_optionSystem": "police système", @@ -578,7 +582,7 @@ "artistConfiguration": "page de configuration de l'artiste de l'album", "artistConfiguration_description": "configurer les éléments et l'ordre à afficher, sur la page de l'artiste de l'album", "doubleClickBehavior": "mettre en file d'attente toutes les pistes recherchées lors d'un double clic", - "contextMenu": "configuration du menu contexte (clic droit)", + "contextMenu": "configuration du menu contextuel (clic droit)", "contextMenu_description": "permet de masquer les éléments qui s'affichent dans le menu lorsque vous cliquez avec le bouton droit de la souris sur un élément. les éléments qui ne sont pas cochés seront masqués", "albumBackground": "image d'arrière-plan de l'album", "albumBackground_description": "ajoute une image d'arrière-plan pour les pages de l'album contenant les illustrations de l'album", @@ -615,7 +619,9 @@ "discordPausedStatus_description": "quand activé, le status s'affichera lorsque le lecteur est en pause", "discordPausedStatus": "afficher le status d'activité en pause", "preservePitch": "préserver la hauteur", - "preservePitch_description": "préserver la hauteur lors du changement de la vitesse de lecture" + "preservePitch_description": "préserver la hauteur lors du changement de la vitesse de lecture", + "notify": "activer les notifications des chansons", + "notify_description": "affiche une notification lors du changement de chanson" }, "form": { "deletePlaylist": { diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index adfb919d..4c3f25f0 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -16,7 +16,12 @@ "toggleSmartPlaylistEditor": "attiva/disattiva editor $t(entity.smartPlaylist)", "removeFromFavorites": "rimuovi da $t(entity.favorite_other)", "moveToTop": "sposta in cima", - "moveToBottom": "sposta in fondo" + "moveToBottom": "sposta in fondo", + "moveToNext": "passa al successivo", + "openIn": { + "lastfm": "Apri in Last.fm", + "musicbrainz": "Apri in MusicBrainz" + } }, "common": { "backward": "indietro", @@ -99,7 +104,22 @@ "yes": "si", "random": "casuale", "size": "dimensione", - "note": "nota" + "note": "nota", + "additionalParticipants": "partecipanti aggiuntivi", + "newVersion": "è stata installata una nuova versione ({{version}})", + "viewReleaseNotes": "mostra le note di rilascio", + "albumGain": "guadagno (gain) dell'album", + "albumPeak": "picco di volume dell'album", + "close": "chiudi", + "codec": "codec", + "mbid": "MusicBrainz ID", + "preview": "anteprima", + "reload": "ricarica", + "share": "condividi", + "tags": "tags", + "trackGain": "normalizzazione (gain) del brano", + "trackPeak": "picco di volume del brano", + "translation": "traduzione" }, "player": { "repeat_all": "ripeti coda", @@ -113,7 +133,7 @@ "skip_back": "salta indietro", "favorite": "preferito", "next": "successivo", - "shuffle": "mescola", + "shuffle": "riproduzione casuale", "playbackFetchNoResults": "nessuna canzone trovata", "playbackFetchInProgress": "caricamento canzoni…", "addNext": "aggiungi successivo", @@ -130,7 +150,9 @@ "shuffle_off": "non mescolare", "addLast": "aggiungi in coda", "mute": "silenzia", - "skip_forward": "salta avanti" + "skip_forward": "salta avanti", + "playSimilarSongs": "riproduci brani simili", + "viewQueue": "visualizza coda" }, "setting": { "crossfadeStyle_description": "seleziona lo stile dissolvenza da usare per il player audio", @@ -150,7 +172,7 @@ "skipDuration_description": "imposta la durata da saltare quando vengono usati i pulsanti di salto nella barra del player", "enableRemote_description": "abilita il controllo remoto del server per permettere ad altri dispositivi di controllare l'applicazione", "fontType_optionSystem": "font di sistema", - "mpvExecutablePath_description": "imposta il percorso dell'eseguibile di mpv", + "mpvExecutablePath_description": "imposta il percorso dell'eseguibile mpv. se lasciato vuoto, verrà utilizzato il percorso predefinito", "hotkey_favoriteCurrentSong": "$t(common.currentSong) preferita", "crossfadeStyle": "stile dissolvenza", "sidebarConfiguration": "configurazione barra laterale", @@ -268,7 +290,7 @@ "replayGainMode_description": "aggiusta il volume secondo i valori {{ReplayGain}} salvati nei metadati del file", "showSkipButtons": "mostra pulsanti per saltare", "sampleRate": "frequenza di campionamento", - "sampleRate_description": "seleziona la frequenza di campionamento di output da usare se la frequenza di campionamento selezionata è diversa da quella della del media attuale", + "sampleRate_description": "seleziona la frequenza di campionamento di output da utilizzare se quella selezionata è diversa da quella del file sorgente in riproduzione. Un valore inferiore a 8000 utilizzerà la frequenza predefinita", "hotkey_togglePreviousSongFavorite": "imposta/rimuovi $t(common.previousSong) favorito", "hotkey_unfavoritePreviousSong": "rimuovi $t(common.previousSong) dai preferiti", "showSkipButton_description": "mostra o nascondi i pulsanti per saltare nella barra del player", @@ -293,7 +315,85 @@ "clearQueryCache": "pulisci cache di feishin", "buttonSize_description": "Dimensione bottoni nella barra di riproduzione", "clearCache": "pulisci la cache del browser", - "clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute" + "clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute", + "albumBackground": "immagine di sfondo dell'album", + "albumBackground_description": "aggiunge un'immagine di sfondo per le pagine degli album contenenti l'album art", + "albumBackgroundBlur": "intensità sfocatura immagine di sfondo dell'album", + "albumBackgroundBlur_description": "regola la quantità di sfocatura applicata all'immagine di sfondo dell'album", + "artistConfiguration": "configurazione della pagina artista dell’album", + "artistConfiguration_description": "configurare quali elementi vengono visualizzati, e in quale ordine, nella pagina dell'artista dell'album", + "buttonSize": "dimensione del bottone nella barra di riproduzione", + "clearCacheSuccess": "cache pulita correttamente", + "contextMenu": "configurazione menu contestuale (clic destro)", + "contextMenu_description": "consente di nascondere gli elementi che vengono visualizzati nel menu quando si fa clic destro su un elemento. gli oggetti non selezionati saranno nascosti", + "customCssEnable": "abilita css personalizzato", + "customCssEnable_description": "consente di scrivere css personalizzati.", + "customCssNotice": "Attenzione: sebbene ci sia una certa sanitizzazione (vengono bloccati url() e content:), l’uso di CSS personalizzati può comunque comportare dei rischi modificando l’interfaccia.", + "customCss": "css personalizzato", + "customCss_description": "contenuto CSS personalizzato. Nota: le proprietà content e gli URL remoti non sono consentiti. Di seguito è mostrata un’anteprima del tuo contenuto. Sono presenti anche altri campi non impostati da te a causa della sanitizzazione.", + "discordPausedStatus": "mostra rich presence di Discord quando la riproduzione è in pausa", + "discordPausedStatus_description": "quando abilitato, verrà mostrato lo stato del lettore in standby/pausa (nessun brano in riproduzione)", + "discordListening": "mostra stato come in ascolto", + "discordListening_description": "mostra lo stato come in ascolto invece che in riproduzione", + "discordServeImage": "recupera le immagini di {{discord}} dal server", + "discordServeImage_description": "condividi la copertina per la rich presence di {{discord}} direttamente dal server, disponibile solo per Jellyfin e Navidrome", + "doubleClickBehavior": "aggiungi alla coda tutte le tracce cercate, con un doppio clic", + "doubleClickBehavior_description": "se attivato, tutte le tracce corrispondenti alla ricerca verranno aggiunte alla coda. altrimenti, verrà aggiunta alla coda solo la traccia selezionata", + "externalLinks": "mostra link esterni", + "externalLinks_description": "consente di visualizzare link esterni (Last.fm, MusicBrainz) sulle pagine di artista/album", + "preferLocalLyrics": "utilizza i testi locali", + "preferLocalLyrics_description": "usa i testi locali anziché quelli online, quando disponibili", + "genreBehavior": "comportamento predefinito della pagina genere", + "genreBehavior_description": "determina se cliccando su un genere si apre di default la lista dei brani o degli album", + "homeConfiguration": "configurazione della home page", + "homeConfiguration_description": "configura quali elementi vengono mostrati e in quale ordine nella home page", + "homeFeature": "carosello in evidenza nella home page", + "homeFeature_description": "controlla se mostrare il grande carosello in evidenza nella pagina principale", + "imageAspectRatio": "usa dimensioni originali(aspect ratio) della copertina", + "imageAspectRatio_description": "se abilitato, la copertina verrà mostrata utilizzando le dimesioni originali. per le immagini con rapporto diverso da 1:1, lo spazio residuo resterà vuoto", + "lastfm": "mostra links last.fm", + "lastfm_description": "mostra i link per last.fm sulle pagine di artista/album", + "lastfmApiKey": "{{lastfm}} chiave API", + "lastfmApiKey_description": "chiave API per {{lastfm}}. necessaria per visualizzare le copertine", + "mpvExtraParameters_help": "uno per linea", + "musicbrainz": "mostra links musicbrainz", + "musicbrainz_description": "mostra link a musicbrainz sulle pagine degli artisti/album, se è disponibile un mbid", + "neteaseTranslation": "Abilita traduzioni di NetEase", + "neteaseTranslation_description": "Se abilitato, recupera e mostra i testi tradotti da NetEase, se disponibili.", + "passwordStore": "Archivio di password/segreti", + "passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali.", + "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", + "playerAlbumArtResolution": "risoluzione della copertina nel lettore", + "playerAlbumArtResolution_description": "la risoluzione dell’anteprima della copertina nel lettore in formato grande. valori più alti la rendono più nitida, ma possono rallentare il caricamento. Il valore predefinito è 0, che indica la modalità automatica", + "sidePlayQueueStyle_optionAttached": "fissata", + "sidePlayQueueStyle_optionDetached": "sganciata", + "startMinimized": "avvia minimizzato", + "startMinimized_description": "avvia l'app nella barra di sistema", + "transcodeNote": "ha effetto dopo 1 brano (web) - 2 brani (mpv)", + "transcode": "abilita la transcodifica", + "transcode_description": "abilita la transcodifica in formati diversi", + "playerbarOpenDrawer": "attiva/disattiva schermo intero", + "playerbarOpenDrawer_description": "consente di cliccare sulla barra del lettore per aprire il lettore a schermo intero", + "replayGainClipping": "clipping di {{ReplayGain}}", + "replayGainFallback": "metodo alternativo di {{ReplayGain}}", + "transcodeBitrate": "bitrate per la transcodifica", + "transcodeBitrate_description": "seleziona il bitrate per la transcodifica. 0 significa lasciare che sia il server a scegliere", + "transcodeFormat": "formato per la transcodifica", + "transcodeFormat_description": "seleziona il formato per la transcodifica. se vuoto viene decisco dal server", + "translationApiProvider": "translation api provider", + "translationApiProvider_description": "api provider for translation", + "translationApiKey": "chiave api translation", + "translationApiKey_description": "chiave api per la traduzione (supporta solo endpoint di servizio globali)", + "translationTargetLanguage": "lingua di destinazione della traduzione", + "translationTargetLanguage_description": "lingua di destinazione per la traduzione", + "trayEnabled": "Mostra icona app nella barra di sistema", + "trayEnabled_description": "mostra/nascondi icona app nella barra si sistema. se disabilitato, disattiva anche minimizza/chiudi nella barra di sistema", + "volumeWidth": "larghezza della barra del volume", + "webAudio": "use audio web", + "webAudio_description": "usa audio web. abilita funzionalità avanzate come ReplayGain. disabilita se riscontri problemi", + "preservePitch": "mantieni tono (pitch)", + "preservePitch_description": "mantiene il tono (pitch) durante la modifica della velocità di riproduzione", + "volumeWidth_description": "larghezza del cursore del volume" }, "error": { "remotePortWarning": "riavvia il server per applicare la nuova porta", @@ -314,7 +414,11 @@ "mpvRequired": "MPV richiesto", "audioDeviceFetchError": "si è verificato un errore nel provare ad ottenre i device audio", "invalidServer": "server non valido", - "loginRateError": "troppi tentativi di accesso, per favore riprova tra qualche secondo" + "loginRateError": "troppi tentativi di accesso, per favore riprova tra qualche secondo", + "badAlbum": "stai visualizzando questa pagina perché questa canzone non fa parte di un album. probabilmente vedi questo messaggio perché hai una canzone posizionata direttamente nella cartella principale della tua libreria musicale. jellyfin raggruppa le tracce solo se si trovano all’interno di una cartella.", + "badValue": "opzione non valida \"{{value}}\". valore inesistente", + "networkError": "si è verificato un errore di rete", + "openError": "impossibile aprire il file" }, "filter": { "mostPlayed": "più riprodotti", @@ -372,7 +476,9 @@ "settings": "$t(common.setting_other)", "home": "$t(common.home)", "artists": "$t(entity.artist_other)", - "albumArtists": "$t(entity.albumArtist_other)" + "albumArtists": "$t(entity.albumArtist_other)", + "myLibrary": "la mia libreria", + "shared": "condivisa $t(entity.playlist_other)" }, "fullscreenPlayer": { "config": { @@ -386,11 +492,16 @@ "unsynchronized": "non sinncronizzato", "lyricAlignment": "allineamento testo", "useImageAspectRatio": "usa le proporzioni dell'immagine", - "lyricGap": "gap testo" + "lyricGap": "gap testo", + "dynamicImageBlur": "intensità sfocatura immagine", + "dynamicIsImage": "abilita immagine di sfondo", + "lyricOffset": "ritardo testi (ms)" }, "upNext": "successivamente", "lyrics": "testi", - "related": "correlati" + "related": "correlati", + "visualizer": "visualizzatore audio", + "noLyrics": "nessun testo trovato" }, "appMenu": { "selectServer": "seleziona server", @@ -420,7 +531,13 @@ "addFavorite": "$t(action.addToFavorites)", "play": "$t(player.play)", "numberSelected": "{{count}} selezionati", - "removeFromQueue": "$t(action.removeFromQueue)" + "removeFromQueue": "$t(action.removeFromQueue)", + "download": "download", + "moveToNext": "$t(action.moveToNext)", + "playSimilarSongs": "$t(player.playSimilarSongs)", + "playShuffled": "$t(player.shuffle)", + "shareItem": "condividi elemento", + "showDetails": "mostra info" }, "home": { "mostPlayed": "più riprodotti", @@ -431,22 +548,28 @@ }, "albumDetail": { "moreFromArtist": "di più da questo $t(entity.artist_one)", - "moreFromGeneric": "di più da {{item}}" + "moreFromGeneric": "di più da {{item}}", + "released": "rilasciato" }, "setting": { "playbackTab": "riproduzione", "generalTab": "generale", "hotkeysTab": "tasti a scelta rapida", - "windowTab": "finestra" + "windowTab": "finestra", + "advanced": "avanzate" }, "albumArtistList": { "title": "$t(entity.albumArtist_other)" }, "genreList": { - "title": "$t(entity.genre_other)" + "title": "$t(entity.genre_other)", + "showAlbums": "mostra $t(entity.genre_one) $t(entity.album_other)", + "showTracks": "mostra $t(entity.genre_one) $t(entity.track_other)" }, "trackList": { - "title": "$t(entity.track_other)" + "title": "$t(entity.track_other)", + "artistTracks": "tracce di {{artist}}", + "genreTracks": "\"{{genre}}\" $t(entity.track_other)" }, "globalSearch": { "commands": { @@ -460,7 +583,36 @@ "title": "$t(entity.playlist_other)" }, "albumList": { - "title": "$t(entity.album_other)" + "title": "$t(entity.album_other)", + "artistAlbums": "albums di {{artist}}", + "genreAlbums": "\"{{genre}}\" $t(entity.album_other)" + }, + "albumArtistDetail": { + "about": "Info {{artist}}", + "appearsOn": "compare su", + "recentReleases": "uscite recenti", + "viewDiscography": "mostra discografia", + "relatedArtists": "correlati $t(entity.artist_other)", + "topSongs": "brani migliori", + "topSongsFrom": "brani migliori da {{title}}", + "viewAll": "mostra tutto", + "viewAllTracks": "mostra tutto $t(entity.track_other)" + }, + "manageServers": { + "title": "gestisci servers", + "serverDetails": "dettagli server", + "url": "URL", + "username": "nome utente", + "editServerDetailsTooltip": "modifica dettagli server", + "removeServer": "rimuovi server" + }, + "itemDetail": { + "copyPath": "copia percorso negli appunti", + "copiedPath": "percorso copiato con successo", + "openFile": "mostra traccia nel gestore file" + }, + "playlist": { + "reorder": "riordino abilitato solo quando si ordina per id" } }, "form": { @@ -491,7 +643,7 @@ "error_savePassword": "si è verificato un errore quando si è provato a salvare la password" }, "addToPlaylist": { - "success": "aggiunto {{message}} $t(entity.track_other) a {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "aggiunto $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "title": "aggiungi a $t(entity.playlist_one)", "input_skipDuplicates": "salta duplicati", "input_playlists": "$t(entity.playlist_other)" @@ -502,7 +654,8 @@ }, "queryEditor": { "input_optionMatchAll": "soddisfa tutti", - "input_optionMatchAny": "soddisfa qualsiasi" + "input_optionMatchAny": "soddisfa qualsiasi", + "title": "editor di query" }, "lyricSearch": { "input_name": "$t(common.name)", @@ -510,7 +663,17 @@ "title": "cerca testi" }, "editPlaylist": { - "title": "modifica $t(entity.playlist_one)" + "title": "modifica $t(entity.playlist_one)", + "publicJellyfinNote": "Jellyfin non mostra se una playlist è pubblica o meno. Se vuoi che rimanga pubblica, assicurati di selezionare l’opzione seguente", + "success": "$t(entity.playlist_one) aggiornato con successo" + }, + "shareItem": { + "allowDownloading": "consentire il download", + "description": "descrizione", + "setExpiration": "imposta scadenza", + "success": "link di condivisione copiato negli appunti (o clicca qui per aprirlo)", + "expireInvalid": "la scadenza deve essere nel futuro", + "createFailed": "condivisione fallita (è abilitata la condivisione?)" } }, "table": { @@ -520,11 +683,17 @@ "gap": "$t(common.gap)", "tableColumns": "tabella colonne", "autoFitColumns": "adatta colonne automaticamente", - "size": "$t(common.size)" + "size": "$t(common.size)", + "followCurrentSong": "segui il brano corrente", + "itemGap": "spaziatura tra gli elementi (px)", + "itemSize": "dimensione dell’elemento (px)" }, "view": { "table": "tabella", - "card": "Scheda" + "card": "Scheda", + "grid": "griglia", + "list": "lista", + "poster": "poster" }, "label": { "releaseDate": "data rilascio", @@ -552,7 +721,9 @@ "discNumber": "numero disco", "favorite": "$t(common.favorite)", "year": "$t(common.year)", - "albumArtist": "$t(entity.albumArtist_one)" + "albumArtist": "$t(entity.albumArtist_one)", + "codec": "$t(common.codec)", + "songCount": "$t(entity.track_other)" } }, "column": { @@ -578,7 +749,8 @@ "path": "percorso", "discNumber": "disco", "channels": "$t(common.channel_other)", - "size": "$t(common.size)" + "size": "$t(common.size)", + "codec": "$t(common.codec)" } }, "entity": { @@ -627,6 +799,12 @@ "genreWithCount_other": "{{count}} generi", "trackWithCount_one": "{{count}} traccia", "trackWithCount_many": "{{count}} tracce", - "trackWithCount_other": "{{count}} tracce" + "trackWithCount_other": "{{count}} tracce", + "play_one": "{{count}} riproduzione", + "play_many": "{{count}} riproduzioni", + "play_other": "{{count}} riproduzioni", + "song_one": "traccia", + "song_many": "tracce", + "song_other": "tracce" } } diff --git a/src/i18n/locales/nb-NO.json b/src/i18n/locales/nb-NO.json index 4adc11f0..60368ba7 100644 --- a/src/i18n/locales/nb-NO.json +++ b/src/i18n/locales/nb-NO.json @@ -104,13 +104,14 @@ "year": "år", "yes": "ja", "descending": "synkende", - "dismiss": "avkreft", + "dismiss": "lukk", "delete": "slett", "description": "beskrivelse", "manage": "håndtere", "maximize": "maksimer", "right": "høyre", - "sortOrder": "rekkefølge" + "sortOrder": "rekkefølge", + "tags": "tagger" }, "entity": { "smartPlaylist": "smart $t(entity.playlist_one)", @@ -233,7 +234,7 @@ "addServer": { "ignoreCors": "ignorer cors ($t(common.restartRequired))", "ignoreSsl": "ignorer ssl ($t(common.restartRequired))", - "error_savePassword": "en problem oppstod ved lagring av passord", + "error_savePassword": "et problem oppstod ved lagring av passord", "input_savePassword": "lagre passord", "input_url": "lenke", "input_username": "brukernavn", @@ -269,6 +270,10 @@ "updateServer": { "success": "vellykket oppdatering av serveren", "title": "oppdater server" + }, + "queryEditor": { + "input_optionMatchAll": "match alle", + "input_optionMatchAny": "matche hvilken som helst" } }, "page": { @@ -338,7 +343,7 @@ "lyricGap": "sangtekstavstand", "dynamicImageBlur": "bilduskarphetstørrelse", "lyricAlignment": "sangtekstjustering", - "lyricOffset": "sangtekstjustering (ms)", + "lyricOffset": "sangtekstforskyvning (ms)", "lyricSize": "sangtekststørrelse", "opacity": "absorpsjon", "showLyricMatch": "vis sangteksttreff", @@ -405,7 +410,8 @@ "search": "$t(common.search)", "settings": "$t(common.setting_other)", "shared": "delt $t(entity.playlist_other)", - "artists": "$t(entity.artist_other)" + "artists": "$t(entity.artist_other)", + "myLibrary": "mitt bibliotek" }, "setting": { "generalTab": "generelt", @@ -416,6 +422,9 @@ }, "playlistList": { "title": "$t(entity.playlist_other)" + }, + "playlist": { + "reorder": "omorganisering kun mulig ved sortering på id" } }, "player": { @@ -439,6 +448,68 @@ "queue_moveToTop": "flytt valgte til bunnen", "playbackFetchNoResults": "ingen sanger funnet", "playbackSpeed": "avspillingshastighet", - "playSimilarSongs": "spill lignende sanger" + "playSimilarSongs": "spill lignende sanger", + "skip": "hopp over", + "shuffle": "spill i tilfeldig rekkefølge", + "shuffle_off": "tilfeldig rekkefølge skrudd av", + "skip_back": "hopp bakover", + "skip_forward": "hopp fremover", + "stop": "stopp", + "toggleFullscreenPlayer": "bytt til fullskjermspiller", + "pause": "sett på pause", + "viewQueue": "se kø", + "unfavorite": "fjern fra favoritter" + }, + "setting": { + "accentColor": "aksentfarge", + "accentColor_description": "setter aksentfarge i applikasjonen", + "albumBackground": "album bakgrunnsbilde", + "albumBackgroundBlur": "album bakgrunnsbilde uskarphetsstørrelse", + "albumBackgroundBlur_description": "justerer grad av uskarphet lagt til på album bakgrunnsbilde", + "audioDevice": "lydenhet", + "zoom": "zoomprosent", + "zoom_description": "angir zoomprosent for applikasjonen" + }, + "table": { + "config": { + "label": { + "playCount": "antall avspillinger", + "releaseDate": "utgivelsesdato", + "trackNumber": "spornummer", + "rowIndex": "radindeks", + "dateAdded": "dato lagt til", + "discNumber": "skivenummer", + "lastPlayed": "sist avspilt" + }, + "view": { + "table": "tabell", + "card": "kort", + "grid": "rutenett", + "list": "liste", + "poster": "plakat" + }, + "general": { + "autoFitColumns": "automatisk kolonnetilpasning", + "displayType": "visningstype", + "followCurrentSong": "følg gjeldende sang" + } + }, + "column": { + "releaseYear": "år", + "comment": "kommentar", + "biography": "biografi", + "album": "album", + "albumArtist": "albumartist", + "dateAdded": "dato lagt til", + "discNumber": "skive", + "favorite": "favoritt", + "lastPlayed": "sist avspilt", + "path": "sti", + "playCount": "avspillinger", + "rating": "vurdering", + "releaseDate": "utgivelsesdato", + "title": "tittel", + "trackNumber": "spor" + } } } diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 13795ee2..ccd45ced 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -93,7 +93,9 @@ "albumPeak": "pico do álbum", "trackGain": "ganho da faixa", "additionalParticipants": "participantes adicionais", - "tags": "tags" + "tags": "tags", + "newVersion": "uma nova versão foi instalada ({{version}})", + "viewReleaseNotes": "ver notas de lançamento" }, "action": { "goToPage": "vá para página", @@ -216,7 +218,9 @@ "crossfadeDuration_description": "define a duração do efeito crossfade", "customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de CSS personalizado ainda pode representar riscos ao alterar a interface.", "crossfadeStyle": "estilo do crossfade", - "crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio" + "crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio", + "disableAutomaticUpdates": "desabilitar atualizações automáticas", + "disableLibraryUpdateOnStartup": "desabilitar a verificação de novas versões na inicialização" }, "table": { "config": { @@ -273,7 +277,8 @@ "nowPlaying": "tocando agora", "playlists": "$t(entity.playlist_other)", "search": "$t(common.search)", - "settings": "$t(common.setting_other)" + "settings": "$t(common.setting_other)", + "myLibrary": "minha biblioteca" }, "playlistList": { "title": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/sl.json b/src/i18n/locales/sl.json new file mode 100644 index 00000000..dc637665 --- /dev/null +++ b/src/i18n/locales/sl.json @@ -0,0 +1,647 @@ +{ + "action": { + "addToFavorites": "dodaj na $t(entity.favorite_other)", + "addToPlaylist": "dodaj na $t(entity.playlist_one)", + "clearQueue": "počisti čakalno vrsto", + "createPlaylist": "ustvari $t(entity.playlist_one)", + "deletePlaylist": "izbriši $t(entity.playlist_one)", + "deselectAll": "odizberi vse", + "editPlaylist": "uredi $t(entity.playlist_one)", + "goToPage": "pojdi na stran", + "moveToNext": "pojdi na naslednjo", + "moveToBottom": "pojdi na dno", + "moveToTop": "pojdi na vrh", + "refresh": "$t(common.refresh)", + "removeFromFavorites": "odstrani iz $t(entity.favorite_other)", + "removeFromPlaylist": "odstrani iz seznama predvajanja", + "removeFromQueue": "odstrani iz čakalne vrste", + "setRating": "nastavi oceno", + "toggleSmartPlaylistEditor": "preklopi urejevalnik $t(entity.smartPlaylist)", + "viewPlaylists": "poglej $t(entity.playlist_other)", + "openIn": { + "lastfm": "Odpri v Last.fm", + "musicbrainz": "Odpri v MusicBrainz" + } + }, + "common": { + "action_one": "dejanje", + "action_two": "dejanji", + "action_few": "dejanja", + "action_other": "dejanj", + "add": "dodaj", + "additionalParticipants": "dodatni udeleženci", + "newVersion": "nova verzija je bila nameščena ({{version}})", + "viewReleaseNotes": "poglej zapiske o različici", + "albumGain": "ojačitev albuma", + "albumPeak": "vrh albuma", + "areYouSure": "ali si prepričan?", + "ascending": "naraščajoče", + "backward": "nazaj", + "biography": "biografija", + "bitrate": "bitna hitrost", + "bpm": "unm", + "cancel": "prekliči", + "center": "center", + "channel_one": "kanal", + "channel_two": "kanala", + "channel_few": "kanali", + "channel_other": "kanalov", + "clear": "počisti", + "close": "zapri", + "codec": "kodek", + "collapse": "strni", + "comingSoon": "prihaja kmalu …", + "configure": "prilagodi", + "confirm": "potrdi", + "create": "ustvari", + "currentSong": "trenutna $t(entity.track_one)", + "decrease": "zmanjšaj", + "delete": "izbriši", + "descending": "padajoče", + "description": "opis", + "disable": "onemogoči", + "disc": "disk", + "dismiss": "spreglej", + "duration": "trajanje", + "edit": "uredi", + "enable": "omogoči", + "expand": "razširi", + "favorite": "najljubša", + "filter_one": "filter", + "filter_two": "filtra", + "filter_few": "filtri", + "filter_other": "filtrov", + "filters": "filtri", + "forceRestartRequired": "znova zaženi, da potrdiš spremembe ... zapri obvestilo, da znova zaženeš", + "forward": "naprej", + "gap": "reža", + "home": "domov", + "increase": "povišaj", + "limit": "omeji", + "manage": "upravljaj", + "maximize": "maksimiziraj", + "menu": "meni", + "minimize": "pomanjšaj", + "modified": "spremenjeno", + "mbid": "MusicBrainz identifikator (ID)", + "left": "levo", + "no": "ne", + "none": "noben", + "noResultsFromQuery": "poizvedba ni vrnila rezultatov", + "note": "opomba", + "ok": "ok", + "owner": "lastnik", + "path": "pot", + "playerMustBePaused": "predvajalnik mora biti ustavljen", + "preview": "predogled", + "previousSong": "prejšnja $t(entity.track_one)", + "quit": "izhod", + "random": "naključno", + "rating": "ocena", + "refresh": "osveži", + "reload": "ponovno naloži", + "reset": "ponastavi", + "resetToDefault": "ponastavi na privzeto", + "restartRequired": "zahtevan je ponovni zagon", + "right": "desno", + "save": "shrani", + "saveAndReplace": "shrani in zamenjaj", + "saveAs": "shrani kot", + "search": "išči", + "setting": "nastavitev", + "share": "deli", + "size": "velikost", + "sortOrder": "vrstni red", + "tags": "oznake", + "title": "naslov", + "trackNumber": "skladba", + "trackGain": "glasnost skladbe", + "trackPeak": "vrhunec skladbe", + "translation": "prevod", + "unknown": "neznan", + "version": "verzija", + "year": "leto", + "yes": "da", + "name": "ime" + }, + "entity": { + "album_one": "album", + "album_two": "albuma", + "album_few": "albumi", + "album_other": "albumov", + "albumArtist_one": "izvajalec albuma", + "albumArtist_two": "izvajalec albumov", + "albumArtist_few": "izvajalec albumov", + "albumArtist_other": "izvajalec albumov", + "albumArtistCount_one": "{{count}} izvajalec albuma", + "albumArtistCount_two": "{{count}} izvajalca albuma", + "albumArtistCount_few": "{{count}} izvajalci albuma", + "albumArtistCount_other": "{{count}} izvajalcev albuma", + "albumWithCount_one": "{{count}} album", + "albumWithCount_two": "{{count}} albuma", + "albumWithCount_few": "{{count}} albumi", + "albumWithCount_other": "{{count}} albumov", + "artist_one": "izvajalec", + "artist_two": "izvajalca", + "artist_few": "izvajalci", + "artist_other": "izvajalcev", + "artistWithCount_one": "{{count}} izvajalec", + "artistWithCount_two": "{{count}} izvajalca", + "artistWithCount_few": "{{count}} izvajalci", + "artistWithCount_other": "{{count}} izvajalcev", + "favorite_one": "priljubljen", + "favorite_two": "priljubljena", + "favorite_few": "priljubljeni", + "favorite_other": "priljubljenih", + "folder_one": "mapa", + "folder_two": "mapi", + "folder_few": "mape", + "folder_other": "map", + "folderWithCount_one": "{{count}} mapa", + "folderWithCount_two": "{{count}} mapi", + "folderWithCount_few": "{{count}} mape", + "folderWithCount_other": "{{count}} map", + "genre_one": "zvrst", + "genre_two": "zvrsti", + "genre_few": "zvrsti", + "genre_other": "zvrsti", + "genreWithCount_one": "{{count}} zvrst", + "genreWithCount_two": "{{count}} zvrsti", + "genreWithCount_few": "{{count}} zvrsti", + "genreWithCount_other": "{{count}} zvrsti", + "playlist_one": "seznam predvajanja", + "playlist_two": "seznama predvajanja", + "playlist_few": "seznami predvajanja", + "playlist_other": "seznamov predvajanja", + "play_one": "{{count}} predvajanje", + "play_two": "{{count}} predvajanji", + "play_few": "{{count}} predvajanja", + "play_other": "{{count}} predvajanj", + "playlistWithCount_one": "{{count}} seznam predvajanja", + "playlistWithCount_two": "{{count}} seznama predvajanja", + "playlistWithCount_few": "{{count}} seznami predvajanja", + "playlistWithCount_other": "{{count}} seznamov predvajanja", + "smartPlaylist": "pametni $t(entity.playlist_one)", + "track_one": "skladba", + "track_two": "skladbi", + "track_few": "skladbe", + "track_other": "skladb", + "song_one": "pesem", + "song_two": "pesmi", + "song_few": "pesmi", + "song_other": "pesmi", + "trackWithCount_one": "{{count}} skladba", + "trackWithCount_two": "{{count}} skladbi", + "trackWithCount_few": "{{count}} skladbe", + "trackWithCount_other": "{{count}} skladb" + }, + "error": { + "apiRouteError": "preusmeritev zahteve ni bila mogoča", + "audioDeviceFetchError": "napaka pri poskusu pridobivanja avdio naprav", + "authenticationFailed": "napaka pri avtentikaciji", + "badAlbum": "ta stran je prikazana ker skladba ne pripada nobenemu albumu. skladba se verjetno nahaja na vrhu datotečne strukture direktorija z glasbo. jellyfin razporedi skladbe v skupine samo v primeru, ko se nahajajo v direktoriju.", + "badValue": "neveljavna možnost \"{{value}}\". ta vrednost ne obstaja več", + "credentialsRequired": "zahtevana prijava", + "endpointNotImplementedError": "{{serverType}} ne implementira končne točke {{endpoint}}", + "genericError": "prišlo je do napake", + "invalidServer": "neveljaven strežnik", + "localFontAccessDenied": "dostop do lokalnih pisav je bil zavrnjen", + "loginRateError": "preveč poskusov prijave, prosimo, poskusite čez nekaj sekund", + "mpvRequired": "obvezen MPV", + "networkError": "prišlo je do mrežne napake", + "openError": "datoteke ni mogoče odpreti", + "playbackError": "prišlo je do napake pri poskusu predvajanja skladbe", + "remoteDisableError": "oddaljenega strežnika ni bilo mogoče $t(common.disable)ti", + "remoteEnableError": "oddaljenega strežnika ni bilo mogoče $t(common.enable)ti", + "remotePortError": "pri nastavljanju vrat oddaljenega strežnika je prišlo do napake", + "remotePortWarning": "ponovno zaženite strežnik da aplicirate spremembo strežniških vrat", + "serverNotSelectedError": "izbran ni bil noben strežnik", + "serverRequired": "strežnik zahtevan", + "sessionExpiredError": "vaša seja se je iztekla", + "systemFontError": "napaka pri pridobivanju sistemskih pisav" + }, + "filter": { + "album": "$t(entity.album_one)", + "albumArtist": "$t(entity.albumArtist_one)", + "albumCount": "število $t(entity.album_other)", + "artist": "$t(entity.artist_one)", + "biography": "biografija", + "bitrate": "bitna hitrost", + "bpm": "bpm", + "channels": "$t(common.channel_other)", + "comment": "komentar", + "communityRating": "ocena skupnosti", + "criticRating": "ocena kritikov", + "dateAdded": "dodano", + "disc": "disk", + "duration": "trajanje", + "favorited": "priljubljeno", + "fromYear": "od leta", + "genre": "$t(entity.genre_one)", + "id": "identifikator", + "isCompilation": "je kompilacija", + "isFavorited": "je dodan med priljubljene", + "isPublic": "je javno", + "isRated": "je ocenjen", + "isRecentlyPlayed": "je bil nedavno predvajan", + "lastPlayed": "zadnje predvajano", + "mostPlayed": "najpogosteje predvajano", + "name": "ime", + "note": "opomba", + "owner": "$t(common.owner)", + "path": "pot", + "playCount": "število predvajanj", + "random": "naključno", + "rating": "ocena", + "recentlyAdded": "nedavno dodano", + "recentlyPlayed": "nedavno predvajano", + "recentlyUpdated": "nedavno posodobljeno", + "releaseDate": "datum izida", + "releaseYear": "leto izida", + "search": "išči", + "songCount": "število pesmi", + "title": "naslov", + "toYear": "do leta", + "trackNumber": "skladba" + }, + "form": { + "addServer": { + "error_savePassword": "pri shranjevanju gesla je prišlo do napake", + "ignoreCors": "ignoriraj cors $t(common.restartRequired)", + "ignoreSsl": "ignoriraj ssl $t(common.restartRequired)", + "input_legacyAuthentication": "omogoči legacy avtentikacijo", + "input_name": "ime strežnika", + "input_password": "geslo", + "input_savePassword": "shrani geslo", + "input_url": "url", + "input_username": "uporabniško ime", + "success": "dodajanje strežnika uspešno", + "title": "dodaj strežnik" + }, + "addToPlaylist": { + "input_playlists": "$t(entity.playlist_other)", + "input_skipDuplicates": "preskoči duplikate", + "success": "$t(entity.trackWithCount, {\"count\": {{message}} }) dodan v $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", + "title": "dodaj v $t(entity.playlist_one)" + }, + "createPlaylist": { + "input_description": "$t(common.description)", + "input_name": "$t(common.name)", + "input_owner": "$t(common.owner)", + "input_public": "javno", + "success": "$t(entity.playlist_one) je bil uspešno ustvarjen", + "title": "ustvari $t(entity.playlist_one)" + }, + "deletePlaylist": { + "input_confirm": "vpišite ime $t(entity.playlist_one) za potrditev", + "success": "$t(entity.playlist_one) uspešno izbrisan", + "title": "izbriši $t(entity.playlist_one)" + }, + "editPlaylist": { + "publicJellyfinNote": "Jellyfin ne poda informacij o tem, ali gre za javni ali zasebni seznam predvajanja. Če želite, da seznam predvajanja ostane javen, izberite naslednji vnos", + "success": "$t(entity.playlist_one) uspešno posodobljen", + "title": "uredi $t(entity.playlist_one)" + }, + "lyricSearch": { + "input_artist": "$t(entity.artist_one)", + "input_name": "$t(common.name)", + "title": "iskanje po besedilu" + }, + "queryEditor": { + "title": "urejevalnik poizvedb", + "input_optionMatchAll": "ujemanje vseh", + "input_optionMatchAny": "ujemanje z najmanj enim" + }, + "shareItem": { + "allowDownloading": "dovoli prenašanje", + "description": "opis", + "setExpiration": "nastavi datum poteka veljavnosti", + "success": "deli povezavo v odložišču (ali klikni tukaj za odpiranje)", + "expireInvalid": "datum poteka veljavnosti mora biti v prihodnosti", + "createFailed": "deljenje ni uspelo (je deljenje omogočeno?)" + }, + "updateServer": { + "success": "strežnik uspešno posodobljen", + "title": "posodobi strežnik" + } + }, + "page": { + "albumArtistDetail": { + "about": "O izvajalcu", + "appearsOn": "se pojavi na", + "recentReleases": "zadnje izdaje", + "viewDiscography": "poglej diskografijo", + "relatedArtists": "sorodni $t(entity.artist_other)", + "topSongs": "najboljše skladbe", + "topSongsFrom": "najboljše skladbe iz {{title}}", + "viewAll": "poglej vse", + "viewAllTracks": "poglej vse $t(entity.track_other)" + }, + "albumArtistList": { + "title": "$t(entity.albumArtist_other)" + }, + "albumDetail": { + "moreFromArtist": "več od $t(entity.artist_one)", + "moreFromGeneric": "več iz {{item}}", + "released": "izdano" + }, + "albumList": { + "artistAlbums": "albumi izvajalca {{artist}}", + "genreAlbums": "\"{{genre}}\" $t(entity.album_other)", + "title": "$t(entity.album_other)" + }, + "appMenu": { + "collapseSidebar": "skrij stransko vrstico", + "expandSidebar": "razširi stransko vrstico", + "goBack": "nazaj", + "goForward": "naprej", + "manageServers": "urejanje strežnikov", + "openBrowserDevtools": "odpri orodja za razvijalce brskalnika", + "quit": "$t(common.quit)", + "selectServer": "izberi strežnik", + "settings": "$t(common.setting_other)", + "version": "verzija {{version}}" + }, + "manageServers": { + "title": "urejanje strežnikov", + "serverDetails": "podrobosti o strežniku", + "url": "URL", + "username": "uporabniško ime", + "editServerDetailsTooltip": "urejanje podrobnosti strežnika", + "removeServer": "odstrani strežnik" + }, + "contextMenu": { + "addFavorite": "$t(action.addToFavorites)", + "addLast": "$t(player.addLast)", + "addNext": "$t(player.addNext)", + "addToFavorites": "$t(action.addToFavorites)", + "addToPlaylist": "$t(action.addToPlaylist)", + "createPlaylist": "$t(action.createPlaylist)", + "deletePlaylist": "$t(action.deletePlaylist)", + "deselectAll": "$t(action.deselectAll)", + "download": "prenesi", + "moveToNext": "$t(action.moveToNext)", + "moveToBottom": "$t(action.moveToBottom)", + "moveToTop": "$t(action.moveToTop)", + "numberSelected": "{{count}} izbranih", + "play": "$t(player.play)", + "playSimilarSongs": "$t(player.playSimilarSongs)", + "removeFromFavorites": "$t(action.removeFromFavorites)", + "removeFromPlaylist": "$t(action.removeFromPlaylist)", + "removeFromQueue": "$t(action.removeFromQueue)", + "setRating": "$t(action.setRating)", + "playShuffled": "$t(player.shuffle)", + "shareItem": "deli", + "showDetails": "pridobi informacije" + }, + "fullscreenPlayer": { + "config": { + "dynamicBackground": "dinamično ozadje", + "dynamicImageBlur": "velikost zameglitve slike", + "dynamicIsImage": "omogoči sliko v ozadju", + "followCurrentLyric": "sledi besedilu", + "lyricAlignment": "poravnava besedila", + "lyricOffset": "zamik besedila (ms)", + "lyricGap": "razmik besedila", + "lyricSize": "velikost besedila", + "opacity": "prosojnost", + "showLyricMatch": "prikaži ujemanje besedila", + "showLyricProvider": "pokaži ponudnika besedila", + "synchronized": "sinhronizirano", + "unsynchronized": "nesinhronizirano", + "useImageAspectRatio": "uporabi razmerje stranic slike" + }, + "lyrics": "besedilo", + "related": "sorodno", + "upNext": "sledi", + "visualizer": "vizualizator", + "noLyrics": "ni bilo najdenih besedil" + }, + "genreList": { + "showAlbums": "prikaži $t(entity.genre_one) $t(entity.album_other)", + "showTracks": "prikaži $t(entity.genre_one) $t(entity.track_other)", + "title": "$t(entity.genre_other)" + }, + "globalSearch": { + "commands": { + "goToPage": "pojdi na stran", + "searchFor": "išči {{query}}", + "serverCommands": "strežniški ukazi" + }, + "title": "ukazi" + }, + "home": { + "explore": "razišči knjižnico", + "mostPlayed": "najpogosteje predvajano", + "newlyAdded": "zadnje dodane izdaje", + "recentlyPlayed": "nedavno predvajano", + "title": "$t(common.home)" + }, + "itemDetail": { + "copyPath": "kopiraj v odložišče", + "copiedPath": "kopiranje poti uspešno", + "openFile": "prikaži skladbo v upravitelju datotek" + }, + "playlist": { + "reorder": "preurejanje je omogočeno samo pri razvrščanju po identifikatorju" + }, + "playlistList": { + "title": "$t(entity.playlist_other)" + }, + "setting": { + "advanced": "napredno", + "generalTab": "splošno", + "hotkeysTab": "blžnjice", + "playbackTab": "predvajanje", + "windowTab": "okno" + }, + "sidebar": { + "albumArtists": "$t(entity.albumArtist_other)", + "albums": "$t(entity.album_other)", + "artists": "$t(entity.artist_other)", + "folders": "$t(entity.folder_other)", + "genres": "$t(entity.genre_other)", + "home": "$t(common.home)", + "myLibrary": "moja knjižnica", + "nowPlaying": "trenutno se predvaja", + "playlists": "$t(entity.playlist_other)", + "search": "$t(common.search)", + "settings": "$t(common.setting_other)", + "shared": "deljen $t(entity.playlist_other)", + "tracks": "$t(entity.track_other)" + }, + "trackList": { + "artistTracks": "skladbe po {{artist}}", + "genreTracks": "\"{{genre}}\" $t(entity.track_other)", + "title": "$t(entity.track_other)" + } + }, + "player": { + "addLast": "dodaj zadnje", + "addNext": "dodaj naslednje", + "favorite": "dodaj med priljubljene", + "mute": "utišaj", + "muted": "utišano", + "next": "naslednje", + "play": "predvajaj", + "playbackFetchCancel": "akcija traja dlje časa... zaprite obvestilo za preklic", + "playbackFetchInProgress": "nalaganje pesmi…", + "playbackFetchNoResults": "nobena pesem ni bila najdena", + "playbackSpeed": "hitrost predvajanja", + "playRandom": "predvajaj naključno", + "playSimilarSongs": "predvajaj sorodne pesmi", + "previous": "prejšnje", + "queue_clear": "počisti čakalno vrsto", + "queue_moveToBottom": "premakni izbrano na vrh", + "queue_moveToTop": "premakni izbrano na dno", + "queue_remove": "odstrani izbrano", + "repeat": "ponovi", + "repeat_all": "ponovi vse", + "repeat_off": "ne ponavljaj", + "shuffle": "predvajaj v naključnem vrstnem redu", + "shuffle_off": "prevajanje v naključnem vrstnem redu izključeno", + "skip": "preskoči", + "skip_back": "preskoči nazaj", + "skip_forward": "preskoči naprej", + "stop": "ustavi", + "toggleFullscreenPlayer": "preklopi predvajalnik v celozaslonski način", + "unfavorite": "odstrani iz priljubljenih", + "pause": "premor", + "viewQueue": "poglej čakalno vrsto" + }, + "setting": { + "accentColor": "barva poudarka", + "accentColor_description": "nastavi barva poudarka aplikacije", + "albumBackground": "slika ozadja albuma", + "albumBackground_description": "doda sliko ozadja za strani albuma", + "albumBackgroundBlur": "velikost zameglitve slike ozadja albuma", + "albumBackgroundBlur_description": "spremeni moč zameglitve slike ozadja albuma", + "applicationHotkeys": "bližnjične tipke aplikacije", + "applicationHotkeys_description": "konfigurira bližnjične tipke aplikacije. obkljukajte da nastavite globalne bližnjico na tipkovnici (samo na namizju)", + "artistConfiguration": "konfiguracija strani izvajalca albuma", + "artistConfiguration_description": "konfiguriranje vsebine in vrstnega reda prikaza na strani izvajalca albuma", + "audioDevice": "avdio naprava", + "audioDevice_description": "izberite avdio napravo za predvajanje (samo v spletnem predvajalniku)", + "audioExclusiveMode": "avdio način", + "audioExclusiveMode_description": "omogoči način ekskluzivnega predvajanja. V tem načinu je sistem običajno zaklenjen in samo mpv lahko oddaja zvok", + "audioPlayer": "avdio predvajalnik", + "audioPlayer_description": "izberite avdio predvajalnik za predvajanje", + "buttonSize": "velikost gumbov vrstice predvajalnika", + "buttonSize_description": "velikost gumbov v vrstici predvajalnika", + "clearCache": "izbriši začasni pomnilnik", + "clearCache_description": "poleg brisanja feishinovega začasnega pomnilnika bo izbrisan tudi začasni pomnilnik brskalnika. nastavitve in prijavni podatki strežnikov se ohranijo", + "clearQueryCache": "počisti feishinov začasni pomnilnik", + "clearQueryCache_description": "osveži sezname predvajanja, metapodatke in ponastavi shranjena besedila. nastavitve, prijavni podatki za strežnike in slike se ohranijo", + "clearCacheSuccess": "začasni pomnilnik uspešno izbrisan", + "contextMenu": "konfiguracija kontekstnega menija (desni klik)", + "contextMenu_description": "omogoči skrivanje vrstic v meniju, prikazanem ob desnem kliku. odznačeni predmeti bodo skriti", + "crossfadeDuration": "trajanje prehoda", + "crossfadeDuration_description": "nastavi čas trajanja prehoda med pesmimi", + "crossfadeStyle": "tip prehoda", + "crossfadeStyle_description": "izbira tipa efekta prehoda", + "customCssEnable": "omogoči css po meri", + "customCssEnable_description": "omogoča urejanje css-ja po meri.", + "customCssNotice": "Opozorilo: kljub določenim varnostnim ukrepom (prepoved url() in content:) lahko uporaba CSS po meri s spreminjanjem vmesnika še vedno predstavlja tveganje.", + "customCss": "css po meri", + "customCss_description": "vsebina css po meri. Opomba: vsebina in oddaljeni url-ji so prepovedane lastnosti. Spodaj je prikazan predogled vaše vsebine. Dodatna polja, ki jih niste nastavili, so prisotna zaradi prečiščevanja.", + "customFontPath": "pot za pisavo po meri", + "customFontPath_description": "nastavi pot do pisave po meri", + "disableAutomaticUpdates": "onemogoči samodejne posodobitve", + "disableLibraryUpdateOnStartup": "onemogoči prevejranje novih verzij ob zagonu", + "discordApplicationId": "{{discord}} identifikator aplikacije", + "discordApplicationId_description": "identifikator aplikacije za {{discord}} bogato prezenco (privzeto {{defaultId}})", + "discordPausedStatus": "prikaži bogato prezenco med ustavljenim predvajanjem", + "discordPausedStatus_description": "ko je nastavitev omogočena, se bo status prikazal tudi ko je predvajanje začasno zaustavljeno", + "discordIdleStatus": "prikaže stanje mirovanja v bogati prezenci", + "discordIdleStatus_description": "ko je nastavitev omogočena, se bo status posodabljal ko predvajalnik miruje", + "discordListening": "prikaži status poslušanja", + "discordListening_description": "prikaži status poslušanja namesto predvajanja", + "discordRichPresence": "{{discord}} bogata prezenca", + "discordRichPresence_description": "omogoči prikaz statusa predvajanja v {{discord}} bogati prezenci. Oznake slike so: {{icon}}, {{playing}} in {{paused}}", + "discordServeImage": "pošiljaj {{discord}} u slike iz strežnika", + "discordServeImage_description": "deli naslovne slike za {{discord}} bogato prisotnost iz samega strežnika, na voljo samo za jellyfin in navidrome", + "discordUpdateInterval": "interval posodabljanja {{discord}} bogate prezence", + "discordUpdateInterval_description": "čas v sekundah med posameznimi posodobitvami (najmanj 15 sekund)", + "doubleClickBehavior": "dvojni klik doda vse iskane skladbe v čakalno vrsto", + "doubleClickBehavior_description": "če je nastavitev vklopljena se bodo v čakalno vrsto dodale vse skladbe, ki ustrezajo iskanju. v nasprotnem primeru se v čakalno vrsto doda samo izbrana skladba", + "enableRemote": "omogoči oddaljeno upravljanje strežnika", + "enableRemote_description": "omogoči oddaljeno nadzorovanje strežnika in s tem dovoli drugim napravam da upravljajo aplikacijo", + "externalLinks": "prikaži zunanje povezave", + "externalLinks_description": "omogoči prikaz zunanjih povezav (Last.fm, MusicBrainz) na straneh albumov,izvajalcev", + "exitToTray": "minimiziraj", + "exitToTray_description": "ob izhodu se aplikacija minimizira v opravilno vrstico", + "floatingQueueArea": "prikaži območje plavajoče čakalne vrste", + "floatingQueueArea_description": "na desni strani zaslona prikažite ikono za ogled čakalne vrste predvajanja", + "followLyric": "sledenje besedilu", + "followLyric_description": "pomaknite besedilo pesmi do trenutnega položaja predvajanja", + "preferLocalLyrics": "prioritiziraj lokalna besedila", + "preferLocalLyrics_description": "prioritiziraj lokalna besedila pred oddaljenimi, kadar so na voljo", + "font": "pisava", + "font_description": "nastavi pisavo, ki jo bo aplikacija uporabljala", + "fontType": "tip pisave", + "fontType_description": "vgrajena pisava izbere eno od pisav, ki jih ponuja Feishin. sistemska pisava vam omogoča, da izberete katero koli pisavo, ki jo ponuja vaš operacijski sistem. po meri lahko izberete svojo pisavo", + "fontType_optionBuiltIn": "vgrajena pisava", + "fontType_optionCustom": "pisava po meri", + "fontType_optionSystem": "sistemska pisava", + "gaplessAudio": "neprekinjen avdio", + "gaplessAudio_description": "nastavi neprekinjen avdio za mpv", + "gaplessAudio_optionWeak": "šibko (priporočeno)", + "genreBehavior": "privzeto vedenje strani z zvrstmi", + "genreBehavior_description": "določa, ali se ob kliku na zvrst privzeto odpre seznam skladb ali albumov", + "globalMediaHotkeys": "globalne bližnjične tipke za vsebino", + "globalMediaHotkeys_description": "omogočite ali onemogočite uporabo bližnjic za sistemske medije za nadzor predvajanja", + "homeConfiguration": "konfiguracija domače strani", + "homeConfiguration_description": "konfigurirajte, kateri elementi so prikazani na domači strani in v kakšnem vrstnem redu", + "homeFeature": "tekoči trak na domači strani", + "homeFeature_description": "nadzoruje, ali naj se na domači strani prikaže velik tekoči trak", + "hotkey_browserBack": "nazaj (brskalnik)", + "hotkey_browserForward": "naprej (brskalnik)", + "hotkey_favoriteCurrentSong": "dodaj $t(common.currentSong) med priljubljene", + "hotkey_favoritePreviousSong": "dodaj $t(common.previousSong) med priljubljene", + "hotkey_globalSearch": "globalno iskanje", + "hotkey_localSearch": "iskanje na strani", + "hotkey_playbackNext": "naslednja skladba", + "hotkey_playbackPause": "pavza", + "hotkey_playbackPlay": "predvajaj", + "hotkey_playbackPlayPause": "predvajaj / pavza", + "hotkey_playbackPrevious": "prejšnja skladba", + "hotkey_playbackStop": "ustavi", + "hotkey_rate0": "počisti oceno", + "hotkey_rate1": "oceni z 1 zvezdico", + "hotkey_rate2": "oceni z 2 zvezdicama", + "hotkey_rate3": "oceni s 3 zvezdicami", + "hotkey_rate4": "oceni s 4 zvezdicami", + "hotkey_rate5": "oceni s 5 zvezdicami", + "hotkey_skipBackward": "preskoči nazaj", + "hotkey_skipForward": "preskoči naprej", + "hotkey_toggleCurrentSongFavorite": "dodaj/odstrani $t(common.currentSong) iz seznama priljubljenih", + "hotkey_toggleFullScreenPlayer": "preklopi predvajalnik na celozaslonski način", + "hotkey_togglePreviousSongFavorite": "dodaj/odstrani $t(common.previousSong) iz seznama priljubljenih", + "hotkey_toggleQueue": "preklopi čakalno vrsto", + "hotkey_toggleRepeat": "preklopi ponovitve", + "hotkey_toggleShuffle": "preklopi naključni vrstni red predvajanja", + "hotkey_unfavoriteCurrentSong": "odstrani $t(common.currentSong) iz seznama priljubljenih", + "hotkey_unfavoritePreviousSong": "odstrani $t(common.previousSong) iz seznama priljubljenih", + "hotkey_volumeDown": "znižaj glasnost", + "hotkey_volumeMute": "utišaj", + "hotkey_volumeUp": "povišaj glasnost", + "hotkey_zoomIn": "povečaj", + "hotkey_zoomOut": "pomanjšaj", + "imageAspectRatio": "uporabi razmerje stranic izvorne naslovnice", + "imageAspectRatio_description": "če je omogočeno, bo naslovnica prikazana z izvornim razmerjem stranic. za slike, ki niso 1:1, bo preostali prostor prazen", + "language": "jezik", + "language_description": "nastavi jezik aplikacije ($t(common.restartRequired))", + "lastfm": "prikaži last.fm povezave", + "lastfm_description": "prikaži povezave do last.fm na straneh izvajalcev/albumov", + "lastfmApiKey": "API ključ {{lastfm}}", + "lastfmApiKey_description": "API ključ za {{lastfm}}. potreben za naslovnico albuma", + "lyricFetch": "pridobi besedila iz interneta", + "lyricFetch_description": "pridobivanje besedil iz različnih internetnih virov", + "lyricFetchProvider": "ponudniki za pridobivanje besedil", + "lyricFetchProvider_description": "izberite ponudnike, od katerih želite pridobiti besedila. vrstni red ponudnikov je vrstni red, v katerem bodo poizvedovani", + "lyricOffset": "zamik besedila (ms)", + "lyricOffset_description": "zamakni besedilo za določeno število milisekund", + "minimizeToTray": "minimiziraj v sistemsko vrstico", + "minimizeToTray_description": "minimizirajte aplikacijo v sistemsko vrstico" + } +} diff --git a/src/i18n/locales/zh-Hans.json b/src/i18n/locales/zh-Hans.json index a1693206..92201488 100644 --- a/src/i18n/locales/zh-Hans.json +++ b/src/i18n/locales/zh-Hans.json @@ -113,7 +113,9 @@ "additionalParticipants": "其他参与者", "tags": "标签", "viewReleaseNotes": "查看发行说明", - "newVersion": "已安装新版本 ({{version}})" + "newVersion": "已安装新版本 ({{version}})", + "bitDepth": "位深度", + "sampleRate": "采样率" }, "entity": { "albumArtist_other": "专辑艺术家", @@ -407,7 +409,9 @@ "discordPausedStatus": "暂停时显示rich presence", "discordPausedStatus_description": "启用后将在播放器暂停时显示状态", "preservePitch": "保持音高", - "preservePitch_description": "在调整播放速度时保持音高" + "preservePitch_description": "在调整播放速度时保持音高", + "notify": "启用歌曲通知", + "notify_description": "更改当前歌曲时显示通知" }, "error": { "remotePortWarning": "重启服务器使新端口生效", @@ -432,7 +436,8 @@ "badAlbum": "您看到此页面是因为这首歌不是专辑的一部分。如果您的音乐文件夹顶层有一首歌曲,您很可能会遇到此问题。jellyfin 仅对位于文件夹中的曲目进行分组。", "networkError": "发生网络错误", "openError": "无法打开文件", - "badValue": "无效的选项 \"{{value}}\". 此值不再存在" + "badValue": "无效的选项 \"{{value}}\". 此值不再存在", + "notificationDenied": "通知权限被拒绝。此设置无效" }, "filter": { "mostPlayed": "最多播放过", diff --git a/src/main/index.ts b/src/main/index.ts index 66075554..9e064074 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -421,9 +421,6 @@ async function createWindow(first = true): Promise { store.set('fullscreen', mainWindow?.isFullScreen()); if (!exitFromTray && store.get('window_exit_to_tray')) { - if (isMacOS() && !forceQuit) { - exitFromTray = true; - } event.preventDefault(); mainWindow?.hide(); } @@ -432,8 +429,6 @@ async function createWindow(first = true): Promise { event.preventDefault(); saved = true; - getMainWindow()?.webContents.send('renderer-save-queue'); - ipcMain.once('player-save-queue', async (_event, data: Record) => { const queueLocation = join(app.getPath('userData'), 'queue'); const serialized = JSON.stringify(data); @@ -457,12 +452,19 @@ async function createWindow(first = true): Promise { } catch (error) { console.error('error saving queue state: ', error); } finally { - mainWindow?.close(); + if (!isMacOS()) { + mainWindow?.close(); + } if (forceQuit) { app.exit(); } } }); + getMainWindow()?.webContents.send('renderer-save-queue'); + } else { + if (forceQuit) { + app.exit(); + } } }); diff --git a/src/remote/app.tsx b/src/remote/app.tsx index cfd6034c..b5e623c3 100644 --- a/src/remote/app.tsx +++ b/src/remote/app.tsx @@ -22,10 +22,7 @@ export const App = () => { const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT); return ( - + ); diff --git a/src/remote/components/buttons/theme-button.tsx b/src/remote/components/buttons/theme-button.tsx index 2a17dcdc..36a7bc31 100644 --- a/src/remote/components/buttons/theme-button.tsx +++ b/src/remote/components/buttons/theme-button.tsx @@ -18,17 +18,7 @@ export const ThemeButton = () => { }} variant="default" > - {isDark ? ( - - ) : ( - - )} + {isDark ? : } ); }; diff --git a/src/remote/components/remote-container.tsx b/src/remote/components/remote-container.tsx index e29fe624..28018682 100644 --- a/src/remote/components/remote-container.tsx +++ b/src/remote/components/remote-container.tsx @@ -32,17 +32,9 @@ export const RemoteContainer = () => { const debouncedSetRating = debounce(setRating, 400); return ( - + {showImage && ( - + )} @@ -87,10 +79,7 @@ export const RemoteContainer = () => { )} - + { /> {(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
- + debouncedSetRating(0)} @@ -123,10 +109,7 @@ export const RemoteContainer = () => {
)}
- + { variant="default" /> - + { max={100} onChangeEnd={(e) => send({ event: 'volume', volume: e })} rightLabel={ - + {volume ?? 0} } diff --git a/src/remote/components/shell.tsx b/src/remote/components/shell.tsx index 83f3d58c..9905c496 100644 --- a/src/remote/components/shell.tsx +++ b/src/remote/components/shell.tsx @@ -13,16 +13,9 @@ export const Shell = () => { const connected = useConnected(); return ( - + - + { justifySelf: 'flex-start', }} > - + - + @@ -58,10 +42,7 @@ export const Shell = () => { {connected ? ( ) : ( -
+
)} diff --git a/src/remote/components/wrapped-slider.tsx b/src/remote/components/wrapped-slider.tsx index cf6d6e88..30f6d487 100644 --- a/src/remote/components/wrapped-slider.tsx +++ b/src/remote/components/wrapped-slider.tsx @@ -61,10 +61,7 @@ export const WrappedSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe const [seek, setSeek] = useState(0); return ( - + {leftLabel && {leftLabel}} { if (newError !== TIMEOUT_ERROR) { console.error('Error when trying to reauthenticate: ', newError); - limitedFail(currentServer); + + if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') { + console.log( + 'Network error during reauthentication - preserving credentials', + ); + } else { + limitedFail(currentServer); + } } // make sure to pass the error so axios will error later on @@ -360,7 +367,11 @@ axiosClient.interceptors.response.use( }); } - limitedFail(currentServer); + if (isAxiosError(error) && error.code === 'ERR_NETWORK') { + console.log('Network error during authentication - preserving credentials'); + } else { + limitedFail(currentServer); + } } return Promise.reject(error); diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index 21001388..0e7b0913 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -251,6 +251,9 @@ axiosClient.interceptors.response.use( message: data['subsonic-response'].error.message, title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string, }); + + // Since we do status === 200, override this value with the error code + response.status = data['subsonic-response'].error.code; } } diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 0803936f..12fa2399 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -46,6 +46,10 @@ const ALBUM_LIST_SORT_MAPPING: Record { const res = await ssApiClient(apiClientProps).updatePlaylist({ @@ -90,7 +94,7 @@ export const SubsonicController: ControllerEndpoint = { }; } - await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({ + const resp = await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({ query: { c: 'Feishin', f: 'json', @@ -99,6 +103,10 @@ export const SubsonicController: ControllerEndpoint = { }, }); + if (resp.status !== 200) { + throw new Error('Failed to log in'); + } + return { credential, userId: null, @@ -269,7 +277,7 @@ export const SubsonicController: ControllerEndpoint = { albumOffset: query.startIndex, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', + query: query.searchTerm || '', songCount: 0, songOffset: 0, }, @@ -418,11 +426,11 @@ export const SubsonicController: ControllerEndpoint = { while (fetchNextPage) { const res = await ssApiClient(apiClientProps).search3({ query: { - albumCount: 500, + albumCount: MAX_SUBSONIC_ITEMS, albumOffset: startIndex, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', + query: query.searchTerm || '', songCount: 0, songOffset: 0, }, @@ -437,8 +445,7 @@ export const SubsonicController: ControllerEndpoint = { totalRecordCount += albumCount; startIndex += albumCount; - // The max limit size for Subsonic is 500 - fetchNextPage = albumCount === 500; + fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS; } return totalRecordCount; @@ -522,7 +529,7 @@ export const SubsonicController: ControllerEndpoint = { genre: query.genres?.length ? query.genres[0] : undefined, musicFolderId: query.musicFolderId, offset: startIndex, - size: 500, + size: MAX_SUBSONIC_ITEMS, toYear, type, }, @@ -546,8 +553,7 @@ export const SubsonicController: ControllerEndpoint = { totalRecordCount += albumCount; startIndex += albumCount; - // The max limit size for Subsonic is 500 - fetchNextPage = albumCount === 500; + fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS; } return totalRecordCount; @@ -904,7 +910,7 @@ export const SubsonicController: ControllerEndpoint = { albumOffset: 0, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', + query: query.searchTerm || '', songCount: query.limit, songOffset: query.startIndex, }, @@ -1046,7 +1052,7 @@ export const SubsonicController: ControllerEndpoint = { albumOffset: 0, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', + query: query.searchTerm || '', songCount: query.limit, songOffset: query.startIndex, }, @@ -1086,8 +1092,8 @@ export const SubsonicController: ControllerEndpoint = { albumOffset: 0, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', - songCount: 500, + query: query.searchTerm || '', + songCount: MAX_SUBSONIC_ITEMS, songOffset: startIndex, }, }); @@ -1101,8 +1107,7 @@ export const SubsonicController: ControllerEndpoint = { totalRecordCount += songCount; startIndex += songCount; - // The max limit size for Subsonic is 500 - fetchNextPage = songCount === 500; + fetchNextPage = songCount === MAX_SUBSONIC_ITEMS; } return totalRecordCount; @@ -1110,6 +1115,10 @@ export const SubsonicController: ControllerEndpoint = { if (query.genreIds) { let totalRecordCount = 0; + + // Rather than just do `getSongsByGenre` by groups of 500, instead + // jump the offset 10x, and then backtrack on the last chunk. This improves + // performance for extremely large libraries while (fetchNextSection) { const res = await ssApiClient(apiClientProps).getSongsByGenre({ query: { @@ -1128,17 +1137,17 @@ export const SubsonicController: ControllerEndpoint = { if (numberOfResults !== 1) { fetchNextSection = false; - startIndex = sectionIndex === 0 ? 0 : sectionIndex - 5000; + startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE; break; } else { - sectionIndex += 5000; + sectionIndex += SUBSONIC_FAST_BATCH_SIZE; } } while (fetchNextPage) { const res = await ssApiClient(apiClientProps).getSongsByGenre({ query: { - count: 500, + count: MAX_SUBSONIC_ITEMS, genre: query.genreIds[0], musicFolderId: query.musicFolderId, offset: startIndex, @@ -1154,7 +1163,7 @@ export const SubsonicController: ControllerEndpoint = { totalRecordCount = startIndex + numberOfResults; startIndex += numberOfResults; - fetchNextPage = numberOfResults === 500; + fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS; } return totalRecordCount; @@ -1176,6 +1185,9 @@ export const SubsonicController: ControllerEndpoint = { let totalRecordCount = 0; + // Rather than just do `search3` by groups of 500, instead + // jump the offset 10x, and then backtrack on the last chunk. This improves + // performance for extremely large libraries while (fetchNextSection) { const res = await ssApiClient(apiClientProps).search3({ query: { @@ -1183,7 +1195,7 @@ export const SubsonicController: ControllerEndpoint = { albumOffset: 0, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', + query: query.searchTerm || '', songCount: 1, songOffset: sectionIndex, }, @@ -1195,13 +1207,12 @@ export const SubsonicController: ControllerEndpoint = { const numberOfResults = (res.body.searchResult3?.song || []).length || 0; - // Check each batch of 5000 songs to check for data - sectionIndex += 5000; - fetchNextSection = numberOfResults === 1; - - if (!fetchNextSection) { - // fetchNextBlock will be false on the next loop so we need to subtract 5000 * 2 - startIndex = sectionIndex - 10000; + if (numberOfResults !== 1) { + fetchNextSection = false; + startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE; + break; + } else { + sectionIndex += SUBSONIC_FAST_BATCH_SIZE; } } @@ -1212,8 +1223,8 @@ export const SubsonicController: ControllerEndpoint = { albumOffset: 0, artistCount: 0, artistOffset: 0, - query: query.searchTerm || '""', - songCount: 500, + query: query.searchTerm || '', + songCount: MAX_SUBSONIC_ITEMS, songOffset: startIndex, }, }); @@ -1227,8 +1238,7 @@ export const SubsonicController: ControllerEndpoint = { totalRecordCount = startIndex + numberOfResults; startIndex += numberOfResults; - // The max limit size for Subsonic is 500 - fetchNextPage = numberOfResults === 500; + fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS; } return totalRecordCount; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index f7a97344..f3f077b4 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -190,15 +190,8 @@ export const App = () => { }, [language]); return ( - - + + diff --git a/src/renderer/components/audio-player/index.tsx b/src/renderer/components/audio-player/index.tsx index 3f41df28..ddb6dc14 100644 --- a/src/renderer/components/audio-player/index.tsx +++ b/src/renderer/components/audio-player/index.tsx @@ -225,6 +225,28 @@ export const AudioPlayer = forwardRef((props, setIsTransitioning(false); }; + const handleOnError = (playerRef: React.RefObject) => { + return ({ target }: ErrorEvent) => { + const { current: player } = playerRef; + if (!player || !(target instanceof Audio)) { + return; + } + + const { error } = target; + if (error?.code !== MediaError.MEDIA_ERR_DECODE) { + return; + } + + const duration = player.getDuration(); + const currentTime = player.getCurrentTime(); + + // Decode error within last second, handle as track ended + if (duration && duration - currentTime < 1) { + handleOnEnded(); + } + }; + }; + useEffect(() => { if (status === PlayerStatus.PLAYING) { if (currentPlayer === 1) { @@ -424,6 +446,7 @@ export const AudioPlayer = forwardRef((props, muted={muted} // If there is no stream url, we do not need to handle when the audio finishes onEnded={stream1 ? handleOnEnded : undefined} + onError={handleOnError(player1Ref)} onProgress={ playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1 } @@ -443,6 +466,7 @@ export const AudioPlayer = forwardRef((props, height={0} muted={muted} onEnded={stream2 ? handleOnEnded : undefined} + onError={handleOnError(player2Ref)} onProgress={ playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2 } diff --git a/src/renderer/components/card/card-controls.tsx b/src/renderer/components/card/card-controls.tsx index db3df51b..c01ac30d 100644 --- a/src/renderer/components/card/card-controls.tsx +++ b/src/renderer/components/card/card-controls.tsx @@ -47,10 +47,7 @@ export const CardControls = ({ return (
- diff --git a/src/renderer/components/card/poster-card.tsx b/src/renderer/components/card/poster-card.tsx index 24e42241..ba314d8b 100644 --- a/src/renderer/components/card/poster-card.tsx +++ b/src/renderer/components/card/poster-card.tsx @@ -55,14 +55,8 @@ export const PosterCard = ({ onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - - + +
- +
); } return ( -
+
{(controls?.cardRows || []).map((row, index) => ( - + ))}
diff --git a/src/renderer/components/context-menu/context-menu.tsx b/src/renderer/components/context-menu/context-menu.tsx index 9658c464..a1d5f69f 100644 --- a/src/renderer/components/context-menu/context-menu.tsx +++ b/src/renderer/components/context-menu/context-menu.tsx @@ -35,14 +35,8 @@ export const ContextMenuButton = forwardRef( onClick={props.onClick} ref={ref} > - - + + {leftIcon} {children} diff --git a/src/renderer/components/feature-carousel/feature-carousel.tsx b/src/renderer/components/feature-carousel/feature-carousel.tsx index 521ec559..ee6ed5a2 100644 --- a/src/renderer/components/feature-carousel/feature-carousel.tsx +++ b/src/renderer/components/feature-carousel/feature-carousel.tsx @@ -77,11 +77,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => { className={styles.wrapper} to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })} > - + {data && ( { />
- +
{
{currentItem?.albumArtists.slice(0, 1).map((artist) => ( - + {artist.name} ))} diff --git a/src/renderer/components/grid-carousel/grid-carousel.tsx b/src/renderer/components/grid-carousel/grid-carousel.tsx index 0bbf067d..2075003b 100644 --- a/src/renderer/components/grid-carousel/grid-carousel.tsx +++ b/src/renderer/components/grid-carousel/grid-carousel.tsx @@ -60,10 +60,7 @@ const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => { {isValidElement(label) ? ( label ) : ( - + {label} )} @@ -280,11 +277,7 @@ export const SwiperGridCarousel = ({ }, []); return ( - + {title ? ( )} - <div - className={styles.scrollArea} - ref={mergedRef} - {...props} - > + <div className={styles.scrollArea} ref={mergedRef} {...props}> {children} </div> </> diff --git a/src/renderer/components/query-builder/index.tsx b/src/renderer/components/query-builder/index.tsx index 83fcf7db..a87c1cac 100644 --- a/src/renderer/components/query-builder/index.tsx +++ b/src/renderer/components/query-builder/index.tsx @@ -99,10 +99,7 @@ export const QueryBuilder = ({ }; return ( - <Stack - gap="sm" - ml={`${level * 10}px`} - > + <Stack gap="sm" ml={`${level * 10}px`}> <Group gap="sm"> <Select data={FILTER_GROUP_OPTIONS_DATA} @@ -112,12 +109,7 @@ export const QueryBuilder = ({ value={data.type} width="20%" /> - <ActionIcon - icon="add" - onClick={handleAddRule} - size="sm" - variant="subtle" - /> + <ActionIcon icon="add" onClick={handleAddRule} size="sm" variant="subtle" /> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <ActionIcon @@ -150,24 +142,14 @@ export const QueryBuilder = ({ <DropdownMenu.Divider /> <DropdownMenu.Item isDanger - leftSection={ - <Icon - color="error" - icon="refresh" - /> - } + leftSection={<Icon color="error" icon="refresh" />} onClick={onResetFilters} > Reset to default </DropdownMenu.Item> <DropdownMenu.Item isDanger - leftSection={ - <Icon - color="error" - icon="delete" - /> - } + leftSection={<Icon color="error" icon="delete" />} onClick={onClearFilters} > Clear filters diff --git a/src/renderer/components/query-builder/query-builder-option.tsx b/src/renderer/components/query-builder/query-builder-option.tsx index b7503534..f78e2afe 100644 --- a/src/renderer/components/query-builder/query-builder-option.tsx +++ b/src/renderer/components/query-builder/query-builder-option.tsx @@ -48,13 +48,7 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => { /> ); case 'date': - return ( - <TextInput - onChange={onChange} - size="sm" - {...props} - /> - ); + return <TextInput onChange={onChange} size="sm" {...props} />; case 'dateRange': return ( <> @@ -92,21 +86,9 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => { /> ); case 'playlist': - return ( - <Select - data={data} - onChange={onChange} - {...props} - /> - ); + return <Select data={data} onChange={onChange} {...props} />; case 'string': - return ( - <TextInput - onChange={onChange} - size="sm" - {...props} - /> - ); + return <TextInput onChange={onChange} size="sm" {...props} />; default: return <></>; @@ -188,10 +170,7 @@ export const QueryBuilderOption = ({ const ml = (level + 1) * 10; return ( - <Group - gap="sm" - ml={ml} - > + <Group gap="sm" ml={ml}> <Select data={filters} maxWidth={170} diff --git a/src/renderer/components/virtual-grid/grid-card/default-card.tsx b/src/renderer/components/virtual-grid/grid-card/default-card.tsx index b9b2ba63..b337952e 100644 --- a/src/renderer/components/virtual-grid/grid-card/default-card.tsx +++ b/src/renderer/components/virtual-grid/grid-card/default-card.tsx @@ -81,10 +81,7 @@ export const DefaultCard = ({ data?.userFavorite && styles.isFavorite, )} > - <Image - className={styles.image} - src={data?.imageUrl} - /> + <Image className={styles.image} src={data?.imageUrl} /> <GridCardControls handleFavorite={controls.handleFavorite} handlePlayQueueAdd={controls.handlePlayQueueAdd} @@ -95,10 +92,7 @@ export const DefaultCard = ({ /> </div> <div className={styles.detailContainer}> - <CardRows - data={data} - rows={controls.cardRows} - /> + <CardRows data={data} rows={controls.cardRows} /> </div> </div> </div> diff --git a/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx b/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx index 380ecacc..82c9401a 100644 --- a/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx +++ b/src/renderer/components/virtual-grid/grid-card/grid-card-controls.tsx @@ -86,10 +86,7 @@ export const GridCardControls = ({ onClick={handlePlay} variant="filled" > - <Icon - icon="mediaPlay" - size="xl" - /> + <Icon icon="mediaPlay" size="xl" /> </Button> <div className={styles.bottomControls}> {itemType !== LibraryItem.PLAYLIST && ( diff --git a/src/renderer/components/virtual-grid/grid-card/poster-card.tsx b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx index 3c168f7c..fb8d5bef 100644 --- a/src/renderer/components/virtual-grid/grid-card/poster-card.tsx +++ b/src/renderer/components/virtual-grid/grid-card/poster-card.tsx @@ -73,17 +73,11 @@ export const PosterCard = ({ margin: controls.itemGap, }} > - <div - className={styles.linkContainer} - onClick={() => navigate(path)} - > + <div className={styles.linkContainer} onClick={() => navigate(path)}> <div className={`${styles.imageContainer} ${data?.userFavorite ? styles.isFavorite : ''}`} > - <Image - className={styles.image} - src={data?.imageUrl} - /> + <Image className={styles.image} src={data?.imageUrl} /> <GridCardControls handleFavorite={controls.handleFavorite} handlePlayQueueAdd={controls.handlePlayQueueAdd} @@ -95,10 +89,7 @@ export const PosterCard = ({ </div> </div> <div className={styles.detailContainer}> - <CardRows - data={data} - rows={controls.cardRows} - /> + <CardRows data={data} rows={controls.cardRows} /> </div> </div> ); diff --git a/src/renderer/components/virtual-table/cells/album-artist-cell.tsx b/src/renderer/components/virtual-table/cells/album-artist-cell.tsx index 006bd8ad..059de9cd 100644 --- a/src/renderer/components/virtual-table/cells/album-artist-cell.tsx +++ b/src/renderer/components/virtual-table/cells/album-artist-cell.tsx @@ -15,21 +15,14 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => { if (value === undefined) { return ( <CellContainer position="left"> - <Skeleton - height="1rem" - width="80%" - /> + <Skeleton height="1rem" width="80%" /> </CellContainer> ); } return ( <CellContainer position="left"> - <Text - isMuted - overflow="hidden" - size="md" - > + <Text isMuted overflow="hidden" size="md"> {value?.map((item: AlbumArtist | Artist, index: number) => ( <React.Fragment key={`row-${item.id}-${data.uniqueId}`}> {index > 0 && <Separator />} @@ -47,11 +40,7 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => { {item.name || '—'} </Text> ) : ( - <Text - isMuted - overflow="hidden" - size="md" - > + <Text isMuted overflow="hidden" size="md"> {item.name || '—'} </Text> )} diff --git a/src/renderer/components/virtual-table/cells/artist-cell.tsx b/src/renderer/components/virtual-table/cells/artist-cell.tsx index fa8cc53f..bbbb66a7 100644 --- a/src/renderer/components/virtual-table/cells/artist-cell.tsx +++ b/src/renderer/components/virtual-table/cells/artist-cell.tsx @@ -15,21 +15,14 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => { if (value === undefined) { return ( <CellContainer position="left"> - <Skeleton - height="1rem" - width="80%" - /> + <Skeleton height="1rem" width="80%" /> </CellContainer> ); } return ( <CellContainer position="left"> - <Text - isMuted - overflow="hidden" - size="md" - > + <Text isMuted overflow="hidden" size="md"> {value?.map((item: AlbumArtist | Artist, index: number) => ( <React.Fragment key={`row-${item.id}-${data.uniqueId}`}> {index > 0 && <Separator />} @@ -47,11 +40,7 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => { {item.name || '—'} </Text> ) : ( - <Text - isMuted - overflow="hidden" - size="md" - > + <Text isMuted overflow="hidden" size="md"> {item.name || '—'} </Text> )} diff --git a/src/renderer/components/virtual-table/cells/combined-title-cell.tsx b/src/renderer/components/virtual-table/cells/combined-title-cell.tsx index bffe9862..ba21d7e3 100644 --- a/src/renderer/components/virtual-table/cells/combined-title-cell.tsx +++ b/src/renderer/components/virtual-table/cells/combined-title-cell.tsx @@ -41,11 +41,7 @@ export const CombinedTitleCell = ({ > <Skeleton className={styles.image} /> </div> - <Skeleton - className={styles.skeletonMetadata} - height="1rem" - width="80%" - /> + <Skeleton className={styles.skeletonMetadata} height="1rem" width="80%" /> </div> ); } @@ -62,11 +58,7 @@ export const CombinedTitleCell = ({ width: `${(node.rowHeight || 40) - 10}px`, }} > - <Image - alt="cover" - className={styles.image} - src={value.imageUrl} - /> + <Image alt="cover" className={styles.image} src={value.imageUrl} /> <ListCoverControls className={styles.playButton} @@ -77,18 +69,10 @@ export const CombinedTitleCell = ({ /> </div> <div className={styles.metadataWrapper}> - <Text - className="current-song-child" - overflow="hidden" - size="md" - > + <Text className="current-song-child" overflow="hidden" size="md"> {value.name} </Text> - <Text - isMuted - overflow="hidden" - size="md" - > + <Text isMuted overflow="hidden" size="md"> {artists?.length ? ( artists.map((artist: AlbumArtist | Artist, index: number) => ( <React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}> diff --git a/src/renderer/components/virtual-table/cells/full-width-disc-cell.tsx b/src/renderer/components/virtual-table/cells/full-width-disc-cell.tsx index 2ce9a923..0a64c03d 100644 --- a/src/renderer/components/virtual-table/cells/full-width-disc-cell.tsx +++ b/src/renderer/components/virtual-table/cells/full-width-disc-cell.tsx @@ -25,10 +25,7 @@ export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => { return ( <div className={styles.container}> - <Group - justify="space-between" - w="100%" - > + <Group justify="space-between" w="100%"> <Button leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />} onClick={handleToggleDiscNodes} diff --git a/src/renderer/components/virtual-table/cells/generic-cell.tsx b/src/renderer/components/virtual-table/cells/generic-cell.tsx index 0cb3c1c7..637598bc 100644 --- a/src/renderer/components/virtual-table/cells/generic-cell.tsx +++ b/src/renderer/components/virtual-table/cells/generic-cell.tsx @@ -23,10 +23,7 @@ export const GenericCell = ({ value, valueFormatted }: ICellRendererParams, opti if (value === undefined) { return ( <CellContainer position={position || 'left'}> - <Skeleton - height="1rem" - width="80%" - /> + <Skeleton height="1rem" width="80%" /> </CellContainer> ); } @@ -45,12 +42,7 @@ export const GenericCell = ({ value, valueFormatted }: ICellRendererParams, opti {isLink ? displayedValue.value : displayedValue} </Text> ) : ( - <Text - isMuted={!primary} - isNoSelect={false} - overflow="hidden" - size="md" - > + <Text isMuted={!primary} isNoSelect={false} overflow="hidden" size="md"> {displayedValue} </Text> )} diff --git a/src/renderer/components/virtual-table/cells/genre-cell.tsx b/src/renderer/components/virtual-table/cells/genre-cell.tsx index 5365948d..e1755dbc 100644 --- a/src/renderer/components/virtual-table/cells/genre-cell.tsx +++ b/src/renderer/components/virtual-table/cells/genre-cell.tsx @@ -13,11 +13,7 @@ export const GenreCell = ({ data, value }: ICellRendererParams) => { const genrePath = useGenreRoute(); return ( <CellContainer position="left"> - <Text - isMuted - overflow="hidden" - size="md" - > + <Text isMuted overflow="hidden" size="md"> {value?.map((item: AlbumArtist | Artist, index: number) => ( <React.Fragment key={`row-${item.id}-${data.uniqueId}`}> {index > 0 && <Separator />} diff --git a/src/renderer/components/virtual-table/cells/note-cell.tsx b/src/renderer/components/virtual-table/cells/note-cell.tsx index c4ef40da..a094c41a 100644 --- a/src/renderer/components/virtual-table/cells/note-cell.tsx +++ b/src/renderer/components/virtual-table/cells/note-cell.tsx @@ -19,20 +19,14 @@ export const NoteCell = ({ value }: ICellRendererParams) => { if (value === undefined) { return ( <CellContainer position="left"> - <Skeleton - height="1rem" - width="80%" - /> + <Skeleton height="1rem" width="80%" /> </CellContainer> ); } return ( <CellContainer position="left"> - <Text - isMuted - overflow="hidden" - > + <Text isMuted overflow="hidden"> {formattedValue} </Text> </CellContainer> diff --git a/src/renderer/components/virtual-table/cells/rating-cell.tsx b/src/renderer/components/virtual-table/cells/rating-cell.tsx index 80111941..b5669e25 100644 --- a/src/renderer/components/virtual-table/cells/rating-cell.tsx +++ b/src/renderer/components/virtual-table/cells/rating-cell.tsx @@ -26,11 +26,7 @@ export const RatingCell = ({ node, value }: ICellRendererParams) => { return ( <CellContainer position="center"> - <Rating - onChange={handleUpdateRating} - size="xs" - value={value?.userRating} - /> + <Rating onChange={handleUpdateRating} size="xs" value={value?.userRating} /> </CellContainer> ); }; diff --git a/src/renderer/components/virtual-table/cells/row-index-cell.tsx b/src/renderer/components/virtual-table/cells/row-index-cell.tsx index 43ca9451..6e9080d2 100644 --- a/src/renderer/components/virtual-table/cells/row-index-cell.tsx +++ b/src/renderer/components/virtual-table/cells/row-index-cell.tsx @@ -144,15 +144,9 @@ export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => { return ( <CellContainer position="right"> {isPlaying && isCurrentSong ? ( - <Icon - fill="primary" - icon="mediaPlay" - /> + <Icon fill="primary" icon="mediaPlay" /> ) : isCurrentSong ? ( - <Icon - fill="primary" - icon="mediaPause" - /> + <Icon fill="primary" icon="mediaPause" /> ) : ( <Text className="current-song-child current-song-index" diff --git a/src/renderer/components/virtual-table/cells/title-cell.tsx b/src/renderer/components/virtual-table/cells/title-cell.tsx index 8afad019..dfc39305 100644 --- a/src/renderer/components/virtual-table/cells/title-cell.tsx +++ b/src/renderer/components/virtual-table/cells/title-cell.tsx @@ -8,21 +8,14 @@ export const TitleCell = ({ value }: ICellRendererParams) => { if (value === undefined) { return ( <CellContainer position="left"> - <Skeleton - height="1rem" - width="80%" - /> + <Skeleton height="1rem" width="80%" /> </CellContainer> ); } return ( <CellContainer position="left"> - <Text - className="current-song-child" - overflow="hidden" - size="md" - > + <Text className="current-song-child" overflow="hidden" size="md"> {value} </Text> </CellContainer> diff --git a/src/renderer/components/virtual-table/headers/duration-header.tsx b/src/renderer/components/virtual-table/headers/duration-header.tsx index c9fa81c9..0b72d792 100644 --- a/src/renderer/components/virtual-table/headers/duration-header.tsx +++ b/src/renderer/components/virtual-table/headers/duration-header.tsx @@ -7,10 +7,5 @@ export interface ICustomHeaderParams extends IHeaderParams { } export const DurationHeader = () => { - return ( - <Icon - icon="duration" - size="sm" - /> - ); + return <Icon icon="duration" size="sm" />; }; diff --git a/src/renderer/components/virtual-table/headers/generic-table-header.tsx b/src/renderer/components/virtual-table/headers/generic-table-header.tsx index 312d935a..27ea385e 100644 --- a/src/renderer/components/virtual-table/headers/generic-table-header.tsx +++ b/src/renderer/components/virtual-table/headers/generic-table-header.tsx @@ -16,36 +16,11 @@ type Options = { type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating'; const headerPresets = { - actions: ( - <Icon - icon="ellipsisHorizontal" - size="sm" - /> - ), - duration: ( - <Icon - icon="duration" - size="sm" - /> - ), - rowIndex: ( - <Icon - icon="hash" - size="sm" - /> - ), - userFavorite: ( - <Icon - icon="favorite" - size="sm" - /> - ), - userRating: ( - <Icon - icon="star" - size="sm" - /> - ), + actions: <Icon icon="ellipsisHorizontal" size="sm" />, + duration: <Icon icon="duration" size="sm" />, + rowIndex: <Icon icon="hash" size="sm" />, + userFavorite: <Icon icon="favorite" size="sm" />, + userRating: <Icon icon="star" size="sm" />, }; export const GenericTableHeader = ( diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 894bba7d..2e06b9f4 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -635,15 +635,8 @@ export const VirtualTable = forwardRef( onNewColumnsLoaded={handleNewColumnsLoaded} /> {paginationProps && ( - <AnimatePresence - initial={false} - mode="wait" - presenceAffectsLayout - > - <TablePagination - {...paginationProps} - tableRef={tableRef} - /> + <AnimatePresence initial={false} mode="wait" presenceAffectsLayout> + <TablePagination {...paginationProps} tableRef={tableRef} /> </AnimatePresence> )} </div> diff --git a/src/renderer/components/virtual-table/table-pagination.tsx b/src/renderer/components/virtual-table/table-pagination.tsx index 63cc9ea2..b59d22af 100644 --- a/src/renderer/components/virtual-table/table-pagination.tsx +++ b/src/renderer/components/virtual-table/table-pagination.tsx @@ -76,10 +76,7 @@ export const TablePagination = ({ ref={containerQuery.ref} style={{ borderTop: '1px solid var(--theme-generic-border-color)' }} > - <Text - isMuted - size="md" - > + <Text isMuted size="md"> {containerQuery.isMd ? ( <> Showing <b>{currentPageStartIndex}</b> - <b>{currentPageStopIndex}</b> of{' '} @@ -97,11 +94,7 @@ export const TablePagination = ({ </> )} </Text> - <Group - gap="sm" - ref={containerQuery.ref} - wrap="nowrap" - > + <Group gap="sm" ref={containerQuery.ref} wrap="nowrap"> <Popover onClose={() => handlers.close()} opened={isGoToPageOpen} @@ -127,10 +120,7 @@ export const TablePagination = ({ min={1} width={70} /> - <Button - type="submit" - variant="filled" - > + <Button type="submit" variant="filled"> Go </Button> </Group> diff --git a/src/renderer/features/action-required/components/action-required-container.tsx b/src/renderer/features/action-required/components/action-required-container.tsx index 6b8fcab3..ac5d78f8 100644 --- a/src/renderer/features/action-required/components/action-required-container.tsx +++ b/src/renderer/features/action-required/components/action-required-container.tsx @@ -13,15 +13,8 @@ interface ActionRequiredContainerProps { export const ActionRequiredContainer = ({ children, title }: ActionRequiredContainerProps) => ( <Stack style={{ cursor: 'default', maxWidth: '700px' }}> <Group> - <Icon - fill="warn" - icon="warn" - size="lg" - /> - <Text - size="xl" - style={{ textTransform: 'uppercase' }} - > + <Icon fill="warn" icon="warn" size="lg" /> + <Text size="xl" style={{ textTransform: 'uppercase' }}> {title} </Text> </Group> diff --git a/src/renderer/features/action-required/components/error-fallback.tsx b/src/renderer/features/action-required/components/error-fallback.tsx index 02b70dcb..cd30dd4e 100644 --- a/src/renderer/features/action-required/components/error-fallback.tsx +++ b/src/renderer/features/action-required/components/error-fallback.tsx @@ -21,18 +21,11 @@ export const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { <Center style={{ height: '100vh' }}> <Stack style={{ maxWidth: '50%' }}> <Group gap="xs"> - <Icon - fill="error" - icon="error" - size="lg" - /> + <Icon fill="error" icon="error" size="lg" /> <Text size="lg">{t('error.genericError')}</Text> </Group> <Text>{error?.message}</Text> - <Button - onClick={resetErrorBoundary} - variant="filled" - > + <Button onClick={resetErrorBoundary} variant="filled"> {t('common.reload')} </Button> </Stack> diff --git a/src/renderer/features/action-required/components/mpv-required.tsx b/src/renderer/features/action-required/components/mpv-required.tsx index 0eb716c2..2907fe29 100644 --- a/src/renderer/features/action-required/components/mpv-required.tsx +++ b/src/renderer/features/action-required/components/mpv-required.tsx @@ -43,18 +43,11 @@ export const MpvRequired = () => { <Text>Set your MPV executable location below and restart the application.</Text> <Text> MPV is available at the following:{' '} - <a - href="https://mpv.io/installation/" - rel="noreferrer" - target="_blank" - > + <a href="https://mpv.io/installation/" rel="noreferrer" target="_blank"> https://mpv.io/ </a> </Text> - <FileInput - disabled={disabled} - onChange={handleSetMpvPath} - /> + <FileInput disabled={disabled} onChange={handleSetMpvPath} /> <Text>{t('setting.disable_mpv', { context: 'description' })}</Text> <Checkbox label={t('setting.disableMpv')} diff --git a/src/renderer/features/action-required/components/route-error-boundary.tsx b/src/renderer/features/action-required/components/route-error-boundary.tsx index d541dffb..5e610be1 100644 --- a/src/renderer/features/action-required/components/route-error-boundary.tsx +++ b/src/renderer/features/action-required/components/route-error-boundary.tsx @@ -42,19 +42,12 @@ const RouteErrorBoundary = () => { px={10} variant="subtle" /> - <Icon - fill="error" - icon="error" - size="lg" - /> + <Icon fill="error" icon="error" size="lg" /> <Text size="lg">{t('error.genericError')}</Text> </Group> <Divider my={5} /> <Text size="sm">{error?.message}</Text> - <Group - gap="sm" - grow - > + <Group gap="sm" grow> <Button leftSection={<Icon icon="home" />} onClick={handleHome} @@ -81,11 +74,7 @@ const RouteErrorBoundary = () => { </DropdownMenu> </Group> <Group grow> - <Button - onClick={handleReload} - size="md" - variant="filled" - > + <Button onClick={handleReload} size="md" variant="filled"> {t('common.reload')} </Button> </Group> diff --git a/src/renderer/features/action-required/components/server-required.tsx b/src/renderer/features/action-required/components/server-required.tsx index aa6b9e2a..45335db1 100644 --- a/src/renderer/features/action-required/components/server-required.tsx +++ b/src/renderer/features/action-required/components/server-required.tsx @@ -132,10 +132,7 @@ function ServerSelector() { }} variant={server.id === currentServer?.id ? 'filled' : 'default'} > - <Group - justify="space-between" - w="100%" - > + <Group justify="space-between" w="100%"> <Group> <img src={logo} @@ -144,10 +141,7 @@ function ServerSelector() { width: 'var(--theme-font-size-2xl)', }} /> - <Text - fw={600} - size="lg" - > + <Text fw={600} size="lg"> {server.name} </Text> </Group> diff --git a/src/renderer/features/action-required/routes/action-required-route.tsx b/src/renderer/features/action-required/routes/action-required-route.tsx index 436e8c8a..9a04ace4 100644 --- a/src/renderer/features/action-required/routes/action-required-route.tsx +++ b/src/renderer/features/action-required/routes/action-required-route.tsx @@ -49,10 +49,7 @@ const ActionRequiredRoute = () => { <AnimatedPage> <PageHeader /> <Center style={{ height: '100%', width: '100vw' }}> - <Stack - gap="xl" - style={{ maxWidth: '50%' }} - > + <Stack gap="xl" style={{ maxWidth: '50%' }}> <Group wrap="nowrap"> {displayedCheck && ( <ActionRequiredContainer title={displayedCheck.title}> @@ -64,10 +61,7 @@ const ActionRequiredRoute = () => { {canReturnHome && <Navigate to={AppRoute.HOME} />} {/* This should be displayed if a credential is required */} {isCredentialRequired && ( - <Group - justify="center" - wrap="nowrap" - > + <Group justify="center" wrap="nowrap"> <Button fullWidth leftSection={<Icon icon="edit" />} diff --git a/src/renderer/features/action-required/routes/invalid-route.tsx b/src/renderer/features/action-required/routes/invalid-route.tsx index 8ac4ea4b..2bac03d0 100644 --- a/src/renderer/features/action-required/routes/invalid-route.tsx +++ b/src/renderer/features/action-required/routes/invalid-route.tsx @@ -18,24 +18,14 @@ const InvalidRoute = () => { <AnimatedPage> <Center style={{ height: '100%', width: '100%' }}> <Stack> - <Group - justify="center" - wrap="nowrap" - > - <Icon - color="warn" - icon="error" - /> + <Group justify="center" wrap="nowrap"> + <Icon color="warn" icon="error" /> <Text size="xl"> {t('error.apiRouteError', { postProcess: 'sentenceCase' })} </Text> </Group> <Text>{location.pathname}</Text> - <ActionIcon - icon="arrowLeftS" - onClick={() => navigate(-1)} - variant="filled" - /> + <ActionIcon icon="arrowLeftS" onClick={() => navigate(-1)} variant="filled" /> </Stack> </Center> </AnimatedPage> diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 88d4fdd4..2d450683 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -319,17 +319,11 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP const mbzId = detailQuery?.data?.mbzId; return ( - <div - className={styles.contentContainer} - ref={cq.ref} - > + <div className={styles.contentContainer} ref={cq.ref}> <LibraryBackgroundOverlay backgroundColor={background} /> <div className={styles.detailContainer}> <section> - <Group - gap="sm" - justify="space-between" - > + <Group gap="sm" justify="space-between"> <Group> <PlayButton onClick={() => handlePlay(playButtonBehavior)} /> <Group gap="xs"> @@ -485,11 +479,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP suppressRowDrag /> </div> - <Stack - gap="lg" - mt="3rem" - ref={cq.ref} - > + <Stack gap="lg" mt="3rem" ref={cq.ref}> {cq.height || cq.width ? ( <> {carousels diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index 2fe52c72..d066098a 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -33,15 +33,9 @@ export const AlbumListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont return ( <Suspense fallback={<Spinner container />}> {display === ListDisplayType.CARD || display === ListDisplayType.GRID ? ( - <AlbumListGridView - gridRef={gridRef} - itemCount={itemCount} - /> + <AlbumListGridView gridRef={gridRef} itemCount={itemCount} /> ) : ( - <AlbumListTableView - itemCount={itemCount} - tableRef={tableRef} - /> + <AlbumListTableView itemCount={itemCount} tableRef={tableRef} /> )} </Suspense> ); diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx index 5c81d3db..0fcdeea6 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -448,11 +448,7 @@ export const AlbumListHeaderFilters = ({ return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button variant="subtle">{sortByLabel}</Button> @@ -471,10 +467,7 @@ export const AlbumListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> <Divider orientation="vertical" /> - <OrderToggleButton - onToggle={handleToggleSortOrder} - sortOrder={filter.sortOrder} - /> + <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} /> {server?.type === ServerType.JELLYFIN && ( <> <Divider orientation="vertical" /> @@ -497,10 +490,7 @@ export const AlbumListHeaderFilters = ({ </DropdownMenu> </> )} - <FilterButton - isActive={!!isFilterApplied} - onClick={handleOpenFiltersModal} - /> + <FilterButton isActive={!!isFilterApplied} onClick={handleOpenFiltersModal} /> <RefreshButton onClick={handleRefresh} /> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> @@ -535,10 +525,7 @@ export const AlbumListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> </Group> - <Group - gap="sm" - wrap="nowrap" - > + <Group gap="sm" wrap="nowrap"> <ListConfigMenu autoFitColumns={table.autoFit} disabledViewTypes={[ListDisplayType.LIST]} diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index 23f50858..daa67237 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -61,15 +61,9 @@ export const AlbumListHeader = ({ }, [filter, genreId, refresh, tableRef]); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader backgroundColor="var(--theme-colors-background)"> - <Flex - justify="space-between" - w="100%" - > + <Flex justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.PlayButton onClick={() => handlePlay?.({ playType: playButtonBehavior })} @@ -85,10 +79,7 @@ export const AlbumListHeader = ({ </LibraryHeaderBar.Badge> </LibraryHeaderBar> <Group> - <SearchInput - defaultValue={filter.searchTerm} - onChange={handleSearch} - /> + <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} /> </Group> </Flex> </PageHeader> diff --git a/src/renderer/features/albums/components/jellyfin-album-filters.tsx b/src/renderer/features/albums/components/jellyfin-album-filters.tsx index 5b81964c..9a76b203 100644 --- a/src/renderer/features/albums/components/jellyfin-album-filters.tsx +++ b/src/renderer/features/albums/components/jellyfin-album-filters.tsx @@ -227,16 +227,9 @@ export const JellyfinAlbumFilters = ({ return ( <Stack p="0.8rem"> {yesNoFilter.map((filter) => ( - <Group - justify="space-between" - key={`nd-filter-${filter.label}`} - > + <Group justify="space-between" key={`nd-filter-${filter.label}`}> <Text>{filter.label}</Text> - <YesNoSelect - onChange={filter.onChange} - size="xs" - value={filter.value} - /> + <YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} /> </Group> ))} <Divider my="0.5rem" /> diff --git a/src/renderer/features/albums/components/navidrome-album-filters.tsx b/src/renderer/features/albums/components/navidrome-album-filters.tsx index efd3cdb2..672c6caf 100644 --- a/src/renderer/features/albums/components/navidrome-album-filters.tsx +++ b/src/renderer/features/albums/components/navidrome-album-filters.tsx @@ -248,28 +248,15 @@ export const NavidromeAlbumFilters = ({ return ( <Stack p="0.8rem"> {yesNoUndefinedFilters.map((filter) => ( - <Group - justify="space-between" - key={`nd-filter-${filter.label}`} - > + <Group justify="space-between" key={`nd-filter-${filter.label}`}> <Text>{filter.label}</Text> - <YesNoSelect - onChange={filter.onChange} - size="xs" - value={filter.value} - /> + <YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} /> </Group> ))} {toggleFilters.map((filter) => ( - <Group - justify="space-between" - key={`nd-filter-${filter.label}`} - > + <Group justify="space-between" key={`nd-filter-${filter.label}`}> <Text>{filter.label}</Text> - <Switch - checked={filter?.value || false} - onChange={filter.onChange} - /> + <Switch checked={filter?.value || false} onChange={filter.onChange} /> </Group> ))} <Divider my="0.5rem" /> @@ -307,10 +294,7 @@ export const NavidromeAlbumFilters = ({ {tagsQuery.data?.enumTags?.length && tagsQuery.data.enumTags.length > 0 && tagsQuery.data.enumTags.map((tag) => ( - <Group - grow - key={tag.name} - > + <Group grow key={tag.name}> <SelectWithInvalidData clearable data={tag.options} diff --git a/src/renderer/features/albums/components/subsonic-album-filters.tsx b/src/renderer/features/albums/components/subsonic-album-filters.tsx index 70702e25..356b5bde 100644 --- a/src/renderer/features/albums/components/subsonic-album-filters.tsx +++ b/src/renderer/features/albums/components/subsonic-album-filters.tsx @@ -148,15 +148,9 @@ export const SubsonicAlbumFilters = ({ return ( <Stack p="0.8rem"> {toggleFilters.map((filter) => ( - <Group - justify="space-between" - key={`nd-filter-${filter.label}`} - > + <Group justify="space-between" key={`nd-filter-${filter.label}`}> <Text>{filter.label}</Text> - <Switch - checked={filter?.value || false} - onChange={filter.onChange} - /> + <Switch checked={filter?.value || false} onChange={filter.onChange} /> </Group> ))} <Divider my="0.5rem" /> diff --git a/src/renderer/features/albums/routes/album-detail-route.tsx b/src/renderer/features/albums/routes/album-detail-route.tsx index cf55f165..32f93675 100644 --- a/src/renderer/features/albums/routes/album-detail-route.tsx +++ b/src/renderer/features/albums/routes/album-detail-route.tsx @@ -70,10 +70,7 @@ const AlbumDetailRoute = () => { }} ref={headerRef} /> - <AlbumDetailContent - background={background} - tableRef={tableRef} - /> + <AlbumDetailContent background={background} tableRef={tableRef} /> </NativeScrollArea> </AnimatedPage> ); diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index 2af3cf4d..095958a6 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -144,11 +144,7 @@ const AlbumListRoute = () => { tableRef={tableRef} title={title} /> - <AlbumListContent - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> + <AlbumListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> </ListContext.Provider> </AnimatedPage> ); diff --git a/src/renderer/features/albums/routes/dummy-album-detail-route.tsx b/src/renderer/features/albums/routes/dummy-album-detail-route.tsx index d5f0b9aa..ccc81636 100644 --- a/src/renderer/features/albums/routes/dummy-album-detail-route.tsx +++ b/src/renderer/features/albums/routes/dummy-album-detail-route.tsx @@ -174,10 +174,7 @@ const DummyAlbumDetailRoute = () => { </Stack> <div className={styles.detailContainer}> <section> - <Group - gap="sm" - justify="space-between" - > + <Group gap="sm" justify="space-between"> <Group> <PlayButton onClick={() => handlePlay()} /> <ActionIcon @@ -231,11 +228,7 @@ const DummyAlbumDetailRoute = () => { <section> <Center> <Group mr={5}> - <Icon - fill="error" - icon="error" - size={30} - /> + <Icon fill="error" icon="error" size={30} /> </Group> <h2>{t('error.badAlbum', { postProcess: 'sentenceCase' })}</h2> </Center> diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index d54066e1..aad0e5b5 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -202,10 +202,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten order: itemOrder.recentAlbums, title: ( <Group align="flex-end"> - <TextTitle - fw={700} - order={2} - > + <TextTitle fw={700} order={2}> {t('page.albumArtistDetail.recentReleases', { postProcess: 'sentenceCase', })} @@ -232,10 +229,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching, order: itemOrder.compilations, title: ( - <TextTitle - fw={700} - order={2} - > + <TextTitle fw={700} order={2}> {t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })} </TextTitle> ), @@ -247,10 +241,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten itemType: LibraryItem.ALBUM_ARTIST, order: itemOrder.similarArtists, title: ( - <TextTitle - fw={700} - order={2} - > + <TextTitle fw={700} order={2}> {t('page.albumArtistDetail.relatedArtists', { postProcess: 'sentenceCase', })} @@ -355,19 +346,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten detailQuery?.isLoading || (server?.type === ServerType.NAVIDROME && enabledItem.topSongs && topSongsQuery?.isLoading); - if (isLoading) - return ( - <div - className={styles.contentContainer} - ref={cq.ref} - /> - ); + if (isLoading) return <div className={styles.contentContainer} ref={cq.ref} />; return ( - <div - className={styles.contentContainer} - ref={cq.ref} - > + <div className={styles.contentContainer} ref={cq.ref}> <LibraryBackgroundOverlay backgroundColor={background} /> <div className={styles.detailContainer}> <Group gap="md"> @@ -481,15 +463,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten ) : null} <Grid gutter="xl"> {biography ? ( - <Grid.Col - order={itemOrder.biography} - span={12} - > + <Grid.Col order={itemOrder.biography} span={12}> <section style={{ maxWidth: '1280px' }}> - <TextTitle - fw={700} - order={2} - > + <TextTitle fw={700} order={2}> {t('page.albumArtistDetail.about', { artist: detailQuery?.data?.name, })} @@ -499,23 +475,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten </Grid.Col> ) : null} {showTopSongs ? ( - <Grid.Col - order={itemOrder.topSongs} - span={12} - > + <Grid.Col order={itemOrder.topSongs} span={12}> <section> - <Group - justify="space-between" - wrap="nowrap" - > - <Group - align="flex-end" - wrap="nowrap" - > - <TextTitle - fw={700} - order={2} - > + <Group justify="space-between" wrap="nowrap"> + <Group align="flex-end" wrap="nowrap"> + <TextTitle fw={700} order={2}> {t('page.albumArtistDetail.topSongs', { postProcess: 'sentenceCase', })} diff --git a/src/renderer/features/artists/components/album-artist-list-content.tsx b/src/renderer/features/artists/components/album-artist-list-content.tsx index a8275b8f..028eff97 100644 --- a/src/renderer/features/artists/components/album-artist-list-content.tsx +++ b/src/renderer/features/artists/components/album-artist-list-content.tsx @@ -42,15 +42,9 @@ export const AlbumArtistListContent = ({ return ( <Suspense fallback={<Spinner container />}> {isGrid ? ( - <AlbumArtistListGridView - gridRef={gridRef} - itemCount={itemCount} - /> + <AlbumArtistListGridView gridRef={gridRef} itemCount={itemCount} /> ) : ( - <AlbumArtistListTableView - itemCount={itemCount} - tableRef={tableRef} - /> + <AlbumArtistListTableView itemCount={itemCount} tableRef={tableRef} /> )} </Suspense> ); diff --git a/src/renderer/features/artists/components/album-artist-list-header-filters.tsx b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx index c61e2191..3ff39318 100644 --- a/src/renderer/features/artists/components/album-artist-list-header-filters.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx @@ -372,11 +372,7 @@ export const AlbumArtistListHeaderFilters = ({ return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button variant="subtle">{sortByLabel}</Button> @@ -395,10 +391,7 @@ export const AlbumArtistListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> <Divider orientation="vertical" /> - <OrderToggleButton - onToggle={handleToggleSortOrder} - sortOrder={filter.sortOrder} - /> + <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} /> {server?.type === ServerType.JELLYFIN && ( <> <DropdownMenu position="bottom-start"> @@ -437,10 +430,7 @@ export const AlbumArtistListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> </Group> - <Group - gap="sm" - wrap="nowrap" - > + <Group gap="sm" wrap="nowrap"> <ListConfigMenu autoFitColumns={table.autoFit} disabledViewTypes={[ListDisplayType.LIST]} diff --git a/src/renderer/features/artists/components/album-artist-list-header.tsx b/src/renderer/features/artists/components/album-artist-list-header.tsx index 293ebf5d..0eea5317 100644 --- a/src/renderer/features/artists/components/album-artist-list-header.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header.tsx @@ -46,15 +46,9 @@ export const AlbumArtistListHeader = ({ }, 500); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader> - <Flex - justify="space-between" - w="100%" - > + <Flex justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.Title> {t('page.albumArtistList.title', { postProcess: 'titleCase' })} @@ -66,18 +60,12 @@ export const AlbumArtistListHeader = ({ </LibraryHeaderBar.Badge> </LibraryHeaderBar> <Group> - <SearchInput - defaultValue={filter.searchTerm} - onChange={handleSearch} - /> + <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} /> </Group> </Flex> </PageHeader> <FilterBar> - <AlbumArtistListHeaderFilters - gridRef={gridRef} - tableRef={tableRef} - /> + <AlbumArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} /> </FilterBar> </Stack> ); diff --git a/src/renderer/features/artists/components/artist-list-content.tsx b/src/renderer/features/artists/components/artist-list-content.tsx index e1332f72..90c22404 100644 --- a/src/renderer/features/artists/components/artist-list-content.tsx +++ b/src/renderer/features/artists/components/artist-list-content.tsx @@ -34,15 +34,9 @@ export const ArtistListContent = ({ gridRef, itemCount, tableRef }: ArtistListCo return ( <Suspense fallback={<Spinner container />}> {isGrid ? ( - <ArtistListGridView - gridRef={gridRef} - itemCount={itemCount} - /> + <ArtistListGridView gridRef={gridRef} itemCount={itemCount} /> ) : ( - <ArtistListTableView - itemCount={itemCount} - tableRef={tableRef} - /> + <ArtistListTableView itemCount={itemCount} tableRef={tableRef} /> )} </Suspense> ); diff --git a/src/renderer/features/artists/components/artist-list-header-filters.tsx b/src/renderer/features/artists/components/artist-list-header-filters.tsx index b1a4c116..456c69db 100644 --- a/src/renderer/features/artists/components/artist-list-header-filters.tsx +++ b/src/renderer/features/artists/components/artist-list-header-filters.tsx @@ -388,11 +388,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button variant="subtle">{sortByLabel}</Button> @@ -411,19 +407,13 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF </DropdownMenu.Dropdown> </DropdownMenu> <Divider orientation="vertical" /> - <OrderToggleButton - onToggle={handleToggleSortOrder} - sortOrder={filter.sortOrder} - /> + <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} /> {server?.type === ServerType.JELLYFIN && ( <> <Divider orientation="vertical" /> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> - <ActionIcon - icon="folder" - variant="subtle" - /> + <ActionIcon icon="folder" variant="subtle" /> </DropdownMenu.Target> <DropdownMenu.Dropdown> {musicFoldersQuery.data?.items.map((folder) => ( @@ -442,11 +432,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF )} {roles.data?.length && ( <> - <Select - data={roles.data} - onChange={handleSetRole} - value={filter.role} - /> + <Select data={roles.data} onChange={handleSetRole} value={filter.role} /> </> )} <RefreshButton onClick={handleRefresh} /> @@ -466,10 +452,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF </DropdownMenu.Dropdown> </DropdownMenu> </Group> - <Group - gap="xs" - wrap="nowrap" - > + <Group gap="xs" wrap="nowrap"> <ListConfigMenu autoFitColumns={table.autoFit} displayType={display} diff --git a/src/renderer/features/artists/components/artist-list-header.tsx b/src/renderer/features/artists/components/artist-list-header.tsx index 51c73174..0a0e65e9 100644 --- a/src/renderer/features/artists/components/artist-list-header.tsx +++ b/src/renderer/features/artists/components/artist-list-header.tsx @@ -42,15 +42,9 @@ export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHea }, 500); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader> - <Flex - justify="space-between" - w="100%" - > + <Flex justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.Title> {t('entity.artist_other', { postProcess: 'titleCase' })} @@ -62,18 +56,12 @@ export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHea </LibraryHeaderBar.Badge> </LibraryHeaderBar> <Group> - <SearchInput - defaultValue={filter.searchTerm} - onChange={handleSearch} - /> + <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} /> </Group> </Flex> </PageHeader> <FilterBar> - <ArtistListHeaderFilters - gridRef={gridRef} - tableRef={tableRef} - /> + <ArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} /> </FilterBar> </Stack> ); diff --git a/src/renderer/features/artists/routes/artist-list-route.tsx b/src/renderer/features/artists/routes/artist-list-route.tsx index e27c4fbe..8113651d 100644 --- a/src/renderer/features/artists/routes/artist-list-route.tsx +++ b/src/renderer/features/artists/routes/artist-list-route.tsx @@ -41,16 +41,8 @@ const ArtistListRoute = () => { return ( <AnimatedPage> <ListContext.Provider value={providerValue}> - <ArtistListHeader - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> - <ArtistListContent - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> + <ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> + <ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> </ListContext.Provider> </AnimatedPage> ); diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 3a00d065..5e33a70f 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -533,10 +533,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { openModal({ children: ( - <ConfirmModal - loading={removeFromPlaylistMutation.isLoading} - onConfirm={confirm} - > + <ConfirmModal loading={removeFromPlaylistMutation.isLoading} onConfirm={confirm}> {t('common.areYouSure', { postProcess: 'sentenceCase' })} </ConfirmModal> ), @@ -922,26 +919,15 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { <Portal> <AnimatePresence> {opened && ( - <ContextMenu - minWidth={125} - ref={mergedRef} - xPos={ctx.xPos} - yPos={ctx.yPos} - > + <ContextMenu minWidth={125} ref={mergedRef} xPos={ctx.xPos} yPos={ctx.yPos}> <Stack gap={0}> - <Stack - gap={0} - onClick={closeContextMenu} - > + <Stack gap={0} onClick={closeContextMenu}> {ctx.menuItems?.map((item) => { return ( !contextMenuItems[item.id].disabled && ( <Fragment key={`context-menu-${item.id}`}> {item.children ? ( - <HoverCard - offset={0} - position="right" - > + <HoverCard offset={0} position="right"> <HoverCard.Target> <ContextMenuButton leftIcon={ diff --git a/src/renderer/features/discord-rpc/use-discord-rpc.ts b/src/renderer/features/discord-rpc/use-discord-rpc.ts index d518e3ca..d2d5dc2a 100644 --- a/src/renderer/features/discord-rpc/use-discord-rpc.ts +++ b/src/renderer/features/discord-rpc/use-discord-rpc.ts @@ -1,10 +1,12 @@ -import { SetActivity } from '@xhayper/discord-rpc'; +import { SetActivity, StatusDisplayType } from '@xhayper/discord-rpc'; import isElectron from 'is-electron'; import { useCallback, useEffect, useState } from 'react'; import { controller } from '/@/renderer/api/controller'; import { + DiscordDisplayType, getServerById, + useAppStore, useDiscordSetttings, useGeneralSettings, usePlayerStore, @@ -17,6 +19,7 @@ const discordRpc = isElectron() ? window.api.discordRpc : null; export const useDiscordRpc = () => { const discordSettings = useDiscordSetttings(); const generalSettings = useGeneralSettings(); + const { privateMode } = useAppStore(); const [lastUniqueId, setlastUniqueId] = useState(''); const setActivity = useCallback( @@ -26,10 +29,8 @@ export const useDiscordRpc = () => { ) => { if ( !current[0] || // No track - (current[0] && - current[2] === 'paused' && // Track paused - (discordSettings.showPaused ? current[1] === 0 : true)) || // Beginning of track (only if show paused setting enabled) - (discordSettings.showPaused ? false : current[1] === 0) // Beginning of track (only if show paused setting disabled) + current[1] === 0 || // Start of track + (current[2] === 'paused' && !discordSettings.showPaused) // Track paused with show paused setting disabled ) return discordRpc?.clearActivity(); @@ -38,11 +39,13 @@ export const useDiscordRpc = () => { const trackChanged = lastUniqueId !== song.uniqueId; /* - 1. If we jump more then 1.2 seconds from last state, update status to match - 2. If the current song id is completely different, update status - 3. If the player state changed, update status + 1. If the song has just started, update status + 2. If we jump more then 1.2 seconds from last state, update status to match + 3. If the current song id is completely different, update status + 4. If the player state changed, update status */ if ( + previous[1] === 0 || Math.abs((current[1] as number) - (previous[1] as number)) > 1.2 || trackChanged || current[2] !== previous[2] @@ -54,6 +57,12 @@ export const useDiscordRpc = () => { const artists = song?.artists.map((artist) => artist.name).join(', '); + const statusDisplayMap = { + [DiscordDisplayType.ARTIST_NAME]: StatusDisplayType.STATE, + [DiscordDisplayType.FEISHIN]: StatusDisplayType.NAME, + [DiscordDisplayType.SONG_NAME]: StatusDisplayType.DETAILS, + }; + const activity: SetActivity = { details: song?.name.padEnd(2, ' ') || 'Idle', instance: false, @@ -61,7 +70,8 @@ export const useDiscordRpc = () => { largeImageText: song?.album || 'Unknown album', smallImageKey: undefined, smallImageText: current[2] as string, - state: (artists && `By ${artists}`) || 'Unknown artist', + state: artists || 'Unknown artist', + statusDisplayType: statusDisplayMap[discordSettings.displayType], // I would love to use the actual type as opposed to hardcoding to 2, // but manually installing the discord-types package appears to break things type: discordSettings.showAsListening ? 2 : 0, @@ -134,20 +144,21 @@ export const useDiscordRpc = () => { discordSettings.showPaused, generalSettings.lastfmApiKey, discordSettings.clientId, + discordSettings.displayType, lastUniqueId, ], ); useEffect(() => { - if (!discordSettings.enabled) return discordRpc?.quit(); + if (!discordSettings.enabled || privateMode) return discordRpc?.quit(); return () => { discordRpc?.quit(); }; - }, [discordSettings.clientId, discordSettings.enabled]); + }, [discordSettings.clientId, privateMode, discordSettings.enabled]); useEffect(() => { - if (!discordSettings.enabled) return; + if (!discordSettings.enabled || privateMode) return; const unsubSongChange = usePlayerStore.subscribe( (state) => [state.current.song, state.current.time, state.current.status], setActivity, @@ -155,5 +166,5 @@ export const useDiscordRpc = () => { return () => { unsubSongChange(); }; - }, [discordSettings.enabled, setActivity]); + }, [discordSettings.enabled, privateMode, setActivity]); }; diff --git a/src/renderer/features/genres/components/genre-list-content.tsx b/src/renderer/features/genres/components/genre-list-content.tsx index 85b3273e..d331479c 100644 --- a/src/renderer/features/genres/components/genre-list-content.tsx +++ b/src/renderer/features/genres/components/genre-list-content.tsx @@ -33,15 +33,9 @@ export const GenreListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont return ( <Suspense fallback={<Spinner container />}> {display === ListDisplayType.CARD || display === ListDisplayType.GRID ? ( - <GenreListGridView - gridRef={gridRef} - itemCount={itemCount} - /> + <GenreListGridView gridRef={gridRef} itemCount={itemCount} /> ) : ( - <GenreListTableView - itemCount={itemCount} - tableRef={tableRef} - /> + <GenreListTableView itemCount={itemCount} tableRef={tableRef} /> )} </Suspense> ); diff --git a/src/renderer/features/genres/components/genre-list-header-filters.tsx b/src/renderer/features/genres/components/genre-list-header-filters.tsx index a7488099..e58fea6e 100644 --- a/src/renderer/features/genres/components/genre-list-header-filters.tsx +++ b/src/renderer/features/genres/components/genre-list-header-filters.tsx @@ -254,11 +254,7 @@ export const GenreListHeaderFilters = ({ return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button variant="subtle">{sortByLabel}</Button> @@ -277,10 +273,7 @@ export const GenreListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> <Divider orientation="vertical" /> - <OrderToggleButton - onToggle={handleToggleSortOrder} - sortOrder={filter.sortOrder} - /> + <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} /> {server?.type === ServerType.JELLYFIN && ( <> <Divider orientation="vertical" /> @@ -340,10 +333,7 @@ export const GenreListHeaderFilters = ({ </Button> </DropdownMenu> </Group> - <Group - gap="sm" - wrap="nowrap" - > + <Group gap="sm" wrap="nowrap"> <ListConfigMenu autoFitColumns={table.autoFit} disabledViewTypes={[ListDisplayType.LIST]} diff --git a/src/renderer/features/genres/components/genre-list-header.tsx b/src/renderer/features/genres/components/genre-list-header.tsx index dc3490ab..e44bbee8 100644 --- a/src/renderer/features/genres/components/genre-list-header.tsx +++ b/src/renderer/features/genres/components/genre-list-header.tsx @@ -40,15 +40,9 @@ export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeade }, 500); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader> - <Flex - justify="space-between" - w="100%" - > + <Flex justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.Title> {t('page.genreList.title', { postProcess: 'titleCase' })} @@ -60,10 +54,7 @@ export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeade </LibraryHeaderBar.Badge> </LibraryHeaderBar> <Group> - <SearchInput - defaultValue={filter.searchTerm} - onChange={handleSearch} - /> + <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} /> </Group> </Flex> </PageHeader> diff --git a/src/renderer/features/genres/routes/genre-list-route.tsx b/src/renderer/features/genres/routes/genre-list-route.tsx index 80650d87..91ebe571 100644 --- a/src/renderer/features/genres/routes/genre-list-route.tsx +++ b/src/renderer/features/genres/routes/genre-list-route.tsx @@ -42,16 +42,8 @@ const GenreListRoute = () => { return ( <AnimatedPage> <ListContext.Provider value={providerValue}> - <GenreListHeader - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> - <GenreListContent - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> + <GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> + <GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> </ListContext.Provider> </AnimatedPage> ); diff --git a/src/renderer/features/item-details/components/item-details-modal.tsx b/src/renderer/features/item-details/components/item-details-modal.tsx index d7e81971..e6bba6be 100644 --- a/src/renderer/features/item-details/components/item-details-modal.tsx +++ b/src/renderer/features/item-details/components/item-details-modal.tsx @@ -81,10 +81,7 @@ const formatArtists = (artists: null | RelatedArtist[] | undefined) => {artist.name || '—'} </Text> ) : ( - <Text - overflow="visible" - size="md" - > + <Text overflow="visible" size="md"> {artist.name || '-'} </Text> )} @@ -119,17 +116,7 @@ const FormatGenre = (item: Album | AlbumArtist | Playlist | Song) => { }; const BoolField = (key: boolean) => - key ? ( - <Icon - color="success" - icon="check" - /> - ) : ( - <Icon - color="error" - icon="x" - /> - ); + key ? <Icon color="success" icon="check" /> : <Icon color="error" icon="x" />; const AlbumPropertyMapping: ItemDetailRow<Album>[] = [ { key: 'name', label: 'common.title' }, @@ -287,6 +274,8 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [ { label: 'filter.isCompilation', render: (song) => BoolField(song.compilation || false) }, { key: 'container', label: 'common.codec' }, { key: 'bitRate', label: 'common.bitrate', render: (song) => `${song.bitRate} kbps` }, + { key: 'sampleRate', label: 'common.sampleRate' }, + { key: 'bitDepth', label: 'common.bitDepth' }, { key: 'channels', label: 'common.channel_other' }, { key: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) }, { @@ -409,12 +398,7 @@ export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => { } return ( - <Table - highlightOnHover - variant="vertical" - withRowBorders={false} - withTableBorder - > + <Table highlightOnHover variant="vertical" withRowBorders={false} withTableBorder> <Table.Tbody>{body}</Table.Tbody> </Table> ); diff --git a/src/renderer/features/item-details/components/song-path.tsx b/src/renderer/features/item-details/components/song-path.tsx index 08bd8eeb..4571f846 100644 --- a/src/renderer/features/item-details/components/song-path.tsx +++ b/src/renderer/features/item-details/components/song-path.tsx @@ -22,10 +22,7 @@ export const SongPath = ({ path }: SongPathProps) => { return ( <Group> - <CopyButton - timeout={2000} - value={path} - > + <CopyButton timeout={2000} value={path}> {({ copied, copy }) => ( <Tooltip label={t( @@ -36,10 +33,7 @@ export const SongPath = ({ path }: SongPathProps) => { )} withinPortal > - <ActionIcon - onClick={copy} - variant="transparent" - > + <ActionIcon onClick={copy} variant="transparent"> {copied ? <Icon icon="check" /> : <Icon icon="clipboardCopy" />} </ActionIcon> </Tooltip> diff --git a/src/renderer/features/lyrics/components/lyrics-search-form.tsx b/src/renderer/features/lyrics/components/lyrics-search-form.tsx index 8cb4db16..55beec9d 100644 --- a/src/renderer/features/lyrics/components/lyrics-search-form.tsx +++ b/src/renderer/features/lyrics/components/lyrics-search-form.tsx @@ -38,33 +38,15 @@ const SearchResult = ({ data, onClick }: SearchResultProps) => { source === LyricSource.GENIUS ? id.replace(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)/g, '') : id; return ( - <button - className={styles.searchItem} - onClick={onClick} - > - <Group - justify="space-between" - wrap="nowrap" - > - <Stack - gap={0} - maw="65%" - > - <Text - fw={600} - size="md" - > + <button className={styles.searchItem} onClick={onClick}> + <Group justify="space-between" wrap="nowrap"> + <Stack gap={0} maw="65%"> + <Text fw={600} size="md"> {name} </Text> <Text isMuted>{artist}</Text> - <Group - gap="sm" - wrap="nowrap" - > - <Text - isMuted - size="sm" - > + <Group gap="sm" wrap="nowrap"> + <Text isMuted size="sm"> {[source, cleanId].join(' — ')} </Text> </Group> @@ -167,11 +149,7 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => { openModal({ children: ( - <LyricsSearchForm - artist={artist} - name={name} - onSearchOverride={onSearchOverride} - /> + <LyricsSearchForm artist={artist} name={name} onSearchOverride={onSearchOverride} /> ), size: 'lg', title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string, diff --git a/src/renderer/features/lyrics/lyrics.tsx b/src/renderer/features/lyrics/lyrics.tsx index 9354d6ab..a578e776 100644 --- a/src/renderer/features/lyrics/lyrics.tsx +++ b/src/renderer/features/lyrics/lyrics.tsx @@ -151,10 +151,7 @@ export const Lyrics = () => { <ErrorBoundary FallbackComponent={ErrorFallback}> <div className={styles.lyricsContainer}> {isLoadingLyrics ? ( - <Spinner - container - size={25} - /> + <Spinner container size={25} /> ) : ( <AnimatePresence mode="sync"> {hasNoLyrics ? ( diff --git a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx index 63511b69..8ed6e4e4 100644 --- a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx +++ b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx @@ -29,10 +29,7 @@ export const UnsynchronizedLyrics = ({ }, [translatedLyrics]); return ( - <div - className={styles.container} - style={{ gap: `${settings.gapUnsync}px` }} - > + <div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}> {settings.showProvider && source && ( <LyricLine alignment={settings.alignment} diff --git a/src/renderer/features/now-playing/components/drawer-play-queue.tsx b/src/renderer/features/now-playing/components/drawer-play-queue.tsx index 03c4cc5e..06f61047 100644 --- a/src/renderer/features/now-playing/components/drawer-play-queue.tsx +++ b/src/renderer/features/now-playing/components/drawer-play-queue.tsx @@ -11,30 +11,17 @@ export const DrawerPlayQueue = () => { const queueRef = useRef<null | { grid: AgGridReactType<Song> }>(null); return ( - <Flex - direction="column" - h="100%" - > + <Flex direction="column" h="100%"> <div style={{ backgroundColor: 'var(--theme-colors-background)', borderRadius: '10px', }} > - <PlayQueueListControls - tableRef={queueRef} - type="sideQueue" - /> + <PlayQueueListControls tableRef={queueRef} type="sideQueue" /> </div> - <Flex - bg="var(--theme-colors-background)" - h="100%" - mb="0.6rem" - > - <PlayQueue - ref={queueRef} - type="sideQueue" - /> + <Flex bg="var(--theme-colors-background)" h="100%" mb="0.6rem"> + <PlayQueue ref={queueRef} type="sideQueue" /> </Flex> </Flex> ); diff --git a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx index afb0d550..e85c2008 100644 --- a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx +++ b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx @@ -174,10 +174,7 @@ export const PlayQueueListControls = ({ tableRef, type }: PlayQueueListOptionsPr /> </Group> <Group> - <Popover - position="top-end" - transitionProps={{ transition: 'fade' }} - > + <Popover position="top-end" transitionProps={{ transition: 'fade' }}> <Popover.Target> <ActionIcon icon="settings" diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx index b4dcca49..89c165c2 100644 --- a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx +++ b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx @@ -18,19 +18,10 @@ export const SidebarPlayQueue = () => { const isWeb = windowBarStyle === Platform.WEB; return ( <VirtualGridContainer> - <Box - display={!isWeb ? 'flex' : undefined} - h="65px" - > - <PlayQueueListControls - tableRef={queueRef} - type="sideQueue" - /> + <Box display={!isWeb ? 'flex' : undefined} h="65px"> + <PlayQueueListControls tableRef={queueRef} type="sideQueue" /> </Box> - <PlayQueue - ref={queueRef} - type="sideQueue" - /> + <PlayQueue ref={queueRef} type="sideQueue" /> </VirtualGridContainer> ); }; diff --git a/src/renderer/features/now-playing/routes/now-playing-route.tsx b/src/renderer/features/now-playing/routes/now-playing-route.tsx index ad2bb098..f20f16ad 100644 --- a/src/renderer/features/now-playing/routes/now-playing-route.tsx +++ b/src/renderer/features/now-playing/routes/now-playing-route.tsx @@ -16,14 +16,8 @@ const NowPlayingRoute = () => { <AnimatedPage> <VirtualGridContainer> <NowPlayingHeader /> - <PlayQueueListControls - tableRef={queueRef} - type="nowPlaying" - /> - <PlayQueue - ref={queueRef} - type="nowPlaying" - /> + <PlayQueueListControls tableRef={queueRef} type="nowPlaying" /> + <PlayQueue ref={queueRef} type="nowPlaying" /> </VirtualGridContainer> </AnimatedPage> ); diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index 7d75791d..adc5d380 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -115,13 +115,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { <div className={styles.controlsContainer}> <div className={styles.buttonsContainer}> <PlayerButton - icon={ - <Icon - fill="default" - icon="mediaStop" - size={buttonSize - 2} - /> - } + icon={<Icon fill="default" icon="mediaStop" size={buttonSize - 2} />} onClick={handleStop} tooltip={{ label: t('player.stop', { postProcess: 'sentenceCase' }), @@ -152,13 +146,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { variant="tertiary" /> <PlayerButton - icon={ - <Icon - fill="default" - icon="mediaPrevious" - size={buttonSize} - /> - } + icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />} onClick={handlePrevTrack} tooltip={{ label: t('player.previous', { postProcess: 'sentenceCase' }), @@ -169,11 +157,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { {skip?.enabled && ( <PlayerButton icon={ - <Icon - fill="default" - icon="mediaStepBackward" - size={buttonSize} - /> + <Icon fill="default" icon="mediaStepBackward" size={buttonSize} /> } onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)} tooltip={{ @@ -194,13 +178,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { /> {skip?.enabled && ( <PlayerButton - icon={ - <Icon - fill="default" - icon="mediaStepForward" - size={buttonSize} - /> - } + icon={<Icon fill="default" icon="mediaStepForward" size={buttonSize} />} onClick={() => handleSkipForward(skip?.skipForwardSeconds)} tooltip={{ label: t('player.skip', { @@ -214,13 +192,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { /> )} <PlayerButton - icon={ - <Icon - fill="default" - icon="mediaNext" - size={buttonSize} - /> - } + icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />} onClick={handleNextTrack} tooltip={{ label: t('player.next', { postProcess: 'sentenceCase' }), @@ -231,11 +203,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { <PlayerButton icon={ repeat === PlayerRepeat.ONE ? ( - <Icon - fill="primary" - icon="mediaRepeatOne" - size={buttonSize} - /> + <Icon fill="primary" icon="mediaRepeatOne" size={buttonSize} /> ) : ( <Icon fill={repeat === PlayerRepeat.NONE ? 'default' : 'primary'} @@ -268,13 +236,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { variant="tertiary" /> <PlayerButton - icon={ - <Icon - fill="default" - icon="mediaRandom" - size={buttonSize} - /> - } + icon={<Icon fill="default" icon="mediaRandom" size={buttonSize} />} onClick={() => openShuffleAllModal({ handlePlayQueueAdd, @@ -291,12 +253,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { </div> <div className={styles.sliderContainer}> <div className={styles.sliderValueWrapper}> - <Text - fw={600} - isMuted - isNoSelect - size="xs" - > + <Text fw={600} isMuted isNoSelect size="xs"> {formattedTime} </Text> </div> @@ -324,12 +281,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { /> </div> <div className={styles.sliderValueWrapper}> - <Text - fw={600} - isMuted - isNoSelect - size="xs" - > + <Text fw={600} isMuted isNoSelect size="xs"> {duration} </Text> </div> diff --git a/src/renderer/features/player/components/full-screen-player-image.tsx b/src/renderer/features/player/components/full-screen-player-image.tsx index 7d62d7aa..84db03e6 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -68,11 +68,7 @@ const ImageWithPlaceholder = ({ width: '100%', }} > - <Icon - color="muted" - icon="itemAlbum" - size="25%" - /> + <Icon color="muted" icon="itemAlbum" size="25%" /> </Center> ); } @@ -167,14 +163,8 @@ export const FullScreenPlayerImage = () => { justify="flex-start" p="1rem" > - <div - className={styles.imageContainer} - ref={mainImageRef} - > - <AnimatePresence - initial={false} - mode="sync" - > + <div className={styles.imageContainer} ref={mainImageRef}> + <AnimatePresence initial={false} mode="sync"> {imageState.current === 0 && ( <ImageWithPlaceholder animate="open" @@ -206,18 +196,8 @@ export const FullScreenPlayerImage = () => { )} </AnimatePresence> </div> - <Stack - className={styles.metadataContainer} - gap="md" - maw="100%" - > - <Text - fw={900} - lh="1.2" - overflow="hidden" - size="4xl" - w="100%" - > + <Stack className={styles.metadataContainer} gap="md" maw="100%"> + <Text fw={900} lh="1.2" overflow="hidden" size="4xl" w="100%"> {currentSong?.name} </Text> <Text @@ -257,10 +237,7 @@ export const FullScreenPlayerImage = () => { </Fragment> ))} </Text> - <Group - justify="center" - mt="sm" - > + <Group justify="center" mt="sm"> {currentSong?.container && ( <Badge variant="transparent">{currentSong?.container}</Badge> )} diff --git a/src/renderer/features/player/components/full-screen-player-queue.tsx b/src/renderer/features/player/components/full-screen-player-queue.tsx index 19b6f262..3b458170 100644 --- a/src/renderer/features/player/components/full-screen-player-queue.tsx +++ b/src/renderer/features/player/components/full-screen-player-queue.tsx @@ -76,10 +76,7 @@ export const FullScreenPlayerQueue = () => { justify="center" > {headerItems.map((item) => ( - <div - className={styles.headerItemWrapper} - key={`tab-${item.label}`} - > + <div className={styles.headerItemWrapper} key={`tab-${item.label}`}> <Button flex={1} fw="600" diff --git a/src/renderer/features/player/components/full-screen-player.tsx b/src/renderer/features/player/components/full-screen-player.tsx index 3f4b3d27..1092c860 100644 --- a/src/renderer/features/player/components/full-screen-player.tsx +++ b/src/renderer/features/player/components/full-screen-player.tsx @@ -238,10 +238,7 @@ const Controls = ({ isPageHovered }: ControlsProps) => { })} </Option.Label> <Option.Control> - <Group - w="100%" - wrap="nowrap" - > + <Group w="100%" wrap="nowrap"> <Slider defaultValue={lyricConfig.fontSize} label={(e) => @@ -278,10 +275,7 @@ const Controls = ({ isPageHovered }: ControlsProps) => { })} </Option.Label> <Option.Control> - <Group - w="100%" - wrap="nowrap" - > + <Group w="100%" wrap="nowrap"> <Slider defaultValue={lyricConfig.gap} label={(e) => `Synchronized: ${e}px`} diff --git a/src/renderer/features/player/components/full-screen-similar-songs.tsx b/src/renderer/features/player/components/full-screen-similar-songs.tsx index 9aa31c95..12aea855 100644 --- a/src/renderer/features/player/components/full-screen-similar-songs.tsx +++ b/src/renderer/features/player/components/full-screen-similar-songs.tsx @@ -4,10 +4,5 @@ import { useCurrentSong } from '/@/renderer/store'; export const FullScreenSimilarSongs = () => { const currentSong = useCurrentSong(); - return currentSong?.id ? ( - <SimilarSongsList - fullScreen - song={currentSong} - /> - ) : null; + return currentSong?.id ? <SimilarSongsList fullScreen song={currentSong} /> : null; }; diff --git a/src/renderer/features/player/components/left-controls.tsx b/src/renderer/features/player/components/left-controls.tsx index 86803faa..4019ebe1 100644 --- a/src/renderer/features/player/components/left-controls.tsx +++ b/src/renderer/features/player/components/left-controls.tsx @@ -69,10 +69,7 @@ export const LeftControls = () => { return ( <div className={styles.leftControlsContainer}> <LayoutGroup> - <AnimatePresence - initial={false} - mode="popLayout" - > + <AnimatePresence initial={false} mode="popLayout"> {!hideImage && ( <div className={styles.imageWrapper}> <motion.div @@ -123,19 +120,9 @@ export const LeftControls = () => { </div> )} </AnimatePresence> - <motion.div - className={styles.metadataStack} - layout="position" - > - <div - className={styles.lineItem} - onClick={stopPropagation} - > - <Group - align="center" - gap="xs" - wrap="nowrap" - > + <motion.div className={styles.metadataStack} layout="position"> + <div className={styles.lineItem} onClick={stopPropagation}> + <Group align="center" gap="xs" wrap="nowrap"> <Text component={Link} fw={500} diff --git a/src/renderer/features/player/components/player-button.tsx b/src/renderer/features/player/components/player-button.tsx index 9071c8a0..120fed9e 100644 --- a/src/renderer/features/player/components/player-button.tsx +++ b/src/renderer/features/player/components/player-button.tsx @@ -61,7 +61,7 @@ interface PlayButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> { } export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>( - ({ isPaused, ...props }: PlayButtonProps, ref) => { + ({ isPaused, onClick, ...props }: PlayButtonProps, ref) => { return ( <ActionIcon className={styles.main} @@ -69,6 +69,10 @@ export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>( iconProps={{ size: 'lg', }} + onClick={(e) => { + e.stopPropagation(); + onClick?.(e); + }} ref={ref} tooltip={{ label: isPaused diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 9b34f820..91f0af96 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -193,13 +193,7 @@ export const RightControls = () => { }, [addToFavoritesMutation, removeFromFavoritesMutation, updateRatingMutation]); return ( - <Flex - align="flex-end" - direction="column" - h="100%" - px="1rem" - py="0.5rem" - > + <Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem"> <Group h="calc(100% / 3)"> {showRating && ( <Rating @@ -209,24 +203,17 @@ export const RightControls = () => { /> )} </Group> - <Group - align="center" - gap="xs" - wrap="nowrap" - > - <DropdownMenu - arrowOffset={12} - offset={0} - position="top-end" - width={425} - withArrow - > + <Group align="center" gap="xs" wrap="nowrap"> + <DropdownMenu arrowOffset={12} offset={0} position="top-end" width={425} withArrow> <DropdownMenu.Target> <ActionIcon icon="mediaSpeed" iconProps={{ size: 'lg', }} + onClick={(e) => { + e.stopPropagation(); + }} size="sm" tooltip={{ label: t('player.playbackSpeed', { postProcess: 'sentenceCase' }), @@ -268,7 +255,10 @@ export const RightControls = () => { fill: currentSong?.userFavorite ? 'primary' : undefined, size: 'lg', }} - onClick={() => handleToggleFavorite(currentSong)} + onClick={(e) => { + e.stopPropagation(); + handleToggleFavorite(currentSong); + }} size="sm" tooltip={{ label: currentSong?.userFavorite @@ -283,7 +273,10 @@ export const RightControls = () => { iconProps={{ size: 'lg', }} - onClick={handleToggleQueue} + onClick={(e) => { + e.stopPropagation(); + handleToggleQueue(); + }} size="sm" tooltip={{ label: t('player.viewQueue', { postProcess: 'titleCase' }), @@ -297,7 +290,10 @@ export const RightControls = () => { color: muted ? 'muted' : undefined, size: 'xl', }} - onClick={handleMute} + onClick={(e) => { + e.stopPropagation(); + handleMute(); + }} onWheel={handleVolumeWheel} size="sm" tooltip={{ diff --git a/src/renderer/features/player/components/visualizer.tsx b/src/renderer/features/player/components/visualizer.tsx index 4374e1de..105bdae0 100644 --- a/src/renderer/features/player/components/visualizer.tsx +++ b/src/renderer/features/player/components/visualizer.tsx @@ -33,10 +33,5 @@ export const Visualizer = () => { return () => {}; }, [accent, canvasRef, motion, webAudio]); - return ( - <div - className={styles.container} - ref={canvasRef} - /> - ); + return <div className={styles.container} ref={canvasRef} />; }; diff --git a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts index c478b4b2..b9597b8d 100644 --- a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts +++ b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts @@ -78,6 +78,7 @@ export const useHandlePlayQueueAdd = () => { // Allow this to be undefined for "play shuffled". If undefined, default to 0, // otherwise, choose the selected item in the queue let initialSongIndex: number | undefined; + let toastId: string | null = null; if (byItemType) { let songList: SongListResponse | undefined; @@ -87,9 +88,8 @@ export const useHandlePlayQueueAdd = () => { timeoutIds.current = { ...timeoutIds.current, [fetchId]: setTimeout(() => { - toast.info({ + toastId = toast.info({ autoClose: false, - id: fetchId, message: t('player.playbackFetchCancel', { postProcess: 'sentenceCase', }), @@ -148,7 +148,9 @@ export const useHandlePlayQueueAdd = () => { clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>); delete timeoutIds.current[fetchId]; - toast.hide(fetchId); + if(toastId){ + toast.hide(toastId); + } } catch (err: any) { if (instanceOfCancellationError(err)) { return null; diff --git a/src/renderer/features/player/hooks/use-scrobble.ts b/src/renderer/features/player/hooks/use-scrobble.ts index 88d798b9..bb52cbde 100644 --- a/src/renderer/features/player/hooks/use-scrobble.ts +++ b/src/renderer/features/player/hooks/use-scrobble.ts @@ -1,8 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation'; -import { usePlayerStore } from '/@/renderer/store'; -import { usePlaybackSettings } from '/@/renderer/store/settings.store'; +import { useAppStore, usePlaybackSettings, usePlayerStore } from '/@/renderer/store'; import { QueueSong, ServerType } from '/@/shared/types/domain-types'; import { PlayerStatus } from '/@/shared/types/types'; @@ -34,6 +33,8 @@ Progress Events (Jellyfin only): - Sends the 'progress' scrobble event on an interval */ +type PlayerEvent = [PlayerStatus, number]; + type SongEvent = [QueueSong | undefined, number, 1 | 2]; const checkScrobbleConditions = (args: { @@ -57,13 +58,14 @@ const checkScrobbleConditions = (args: { export const useScrobble = () => { const scrobbleSettings = usePlaybackSettings().scrobble; const isScrobbleEnabled = scrobbleSettings?.enabled; + const isPrivateModeEnabled = useAppStore().privateMode; const sendScrobble = useSendScrobble(); const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(false); const handleScrobbleFromSeek = useCallback( (currentTime: number) => { - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -82,35 +84,48 @@ export const useScrobble = () => { serverId: currentSong?.serverId, }); }, - [isScrobbleEnabled, sendScrobble], + [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], ); const progressIntervalId = useRef<null | ReturnType<typeof setInterval>>(null); - const songChangeTimeoutId = useRef<null | ReturnType<typeof setTimeout>>(null); + const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); + const notifyTimeoutId = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); + const handleScrobbleFromSongChange = useCallback( (current: SongEvent, previous: SongEvent) => { - if (scrobbleSettings?.notify && current[0]) { + if (scrobbleSettings?.notify && current[0]?.id) { + clearTimeout(notifyTimeoutId.current); const currentSong = current[0]; - const artists = - currentSong.artists?.length > 0 - ? currentSong.artists.map((artist) => artist.name).join(', ') - : currentSong.artistName; + // Set a delay so that quickly (within a second) switching songs doesn't trigger multiple + // notifications + notifyTimeoutId.current = setTimeout(() => { + // Only trigger if the song changed, or the player changed. This should be the case + // anyways, but who knows + if ( + currentSong.uniqueId !== previous[0]?.uniqueId || + current[2] !== previous[2] + ) { + const artists = + currentSong.artists?.length > 0 + ? currentSong.artists.map((artist) => artist.name).join(', ') + : currentSong.artistName; - new Notification(`Now playing ${currentSong.name}`, { - body: `by ${artists} on ${currentSong.album}`, - icon: currentSong.imageUrl || undefined, - }); + new Notification(`Now playing ${currentSong.name}`, { + body: `by ${artists} on ${currentSong.album}`, + icon: currentSong.imageUrl || undefined, + }); + } + }, 1000); } - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; if (progressIntervalId.current) { clearInterval(progressIntervalId.current); progressIntervalId.current = null; } - // const currentSong = current[0] as QueueSong | undefined; const previousSong = previous[0]; const previousSongTimeSec = previous[1]; @@ -146,7 +161,7 @@ export const useScrobble = () => { setIsCurrentSongScrobbled(false); // Use a timeout to prevent spamming the server with scrobble events when switching through songs quickly - clearTimeout(songChangeTimeoutId.current as ReturnType<typeof setTimeout>); + clearTimeout(songChangeTimeoutId.current); songChangeTimeoutId.current = setTimeout(() => { const currentSong = current[0]; // Get the current status from the state, not variable. This is because @@ -186,6 +201,7 @@ export const useScrobble = () => { scrobbleSettings?.scrobbleAtDuration, scrobbleSettings?.scrobbleAtPercentage, isScrobbleEnabled, + isPrivateModeEnabled, isCurrentSongScrobbled, sendScrobble, handleScrobbleFromSeek, @@ -193,11 +209,8 @@ export const useScrobble = () => { ); const handleScrobbleFromStatusChange = useCallback( - ( - current: (number | PlayerStatus | undefined)[], - previous: (number | PlayerStatus | undefined)[], - ) => { - if (!isScrobbleEnabled) return; + (current: PlayerEvent, previous: PlayerEvent) => { + if (!isScrobbleEnabled || isPrivateModeEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -208,8 +221,8 @@ export const useScrobble = () => { ? usePlayerStore.getState().current.time * 1e7 : undefined; - const currentStatus = current[0] as PlayerStatus; - const currentTimeSec = current[1] as number; + const currentStatus = current[0]; + const currentTimeSec = current[1]; // Whenever the player is restarted, send a 'start' scrobble if (currentStatus === PlayerStatus.PLAYING) { @@ -249,12 +262,12 @@ export const useScrobble = () => { }); if (progressIntervalId.current) { - clearInterval(progressIntervalId.current as ReturnType<typeof setInterval>); + clearInterval(progressIntervalId.current); progressIntervalId.current = null; } } else { const isLastTrackInQueue = usePlayerStore.getState().actions.checkIsLastTrack(); - const previousTimeSec = previous[1] as number; + const previousTimeSec = previous[1]; // If not already scrobbled, send a 'submission' scrobble if conditions are met const shouldSubmitScrobble = checkScrobbleConditions({ @@ -281,6 +294,7 @@ export const useScrobble = () => { }, [ isScrobbleEnabled, + isPrivateModeEnabled, sendScrobble, handleScrobbleFromSeek, scrobbleSettings?.scrobbleAtDuration, @@ -294,7 +308,7 @@ export const useScrobble = () => { // need to perform another check to see if the scrobble conditions are met const handleScrobbleFromSongRestart = useCallback( (currentTime: number) => { - if (!isScrobbleEnabled) return; + if (!isScrobbleEnabled || isPrivateModeEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -337,6 +351,7 @@ export const useScrobble = () => { }, [ isScrobbleEnabled, + isPrivateModeEnabled, scrobbleSettings?.scrobbleAtDuration, scrobbleSettings?.scrobbleAtPercentage, isCurrentSongScrobbled, @@ -358,17 +373,17 @@ export const useScrobble = () => { // multiple times in a row and playback goes normally (no next/previous) equalityFn: (a, b) => // compute whether the song changed - (a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId && + a[0]?.uniqueId === b[0]?.uniqueId && // compute whether the same player: relevant for repeat one and repeat all (one track) a[2] === b[2], }, ); const unsubStatusChange = usePlayerStore.subscribe( - (state) => [state.current.status, state.current.time], + (state): PlayerEvent => [state.current.status, state.current.time], handleScrobbleFromStatusChange, { - equalityFn: (a, b) => (a[0] as PlayerStatus) === (b[0] as PlayerStatus), + equalityFn: (a, b) => a[0] === b[0], }, ); diff --git a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx index 7e4d4be5..0f24ca23 100644 --- a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx +++ b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx @@ -36,6 +36,7 @@ export const AddToPlaylistContextModal = ({ const { albumId, artistId, genreId, songId } = innerProps; const server = useCurrentServer(); const [isLoading, setIsLoading] = useState(false); + const [isDropdownOpened, setIsDropdownOpened] = useState(true); const addToPlaylistMutation = useAddToPlaylist({}); @@ -235,7 +236,13 @@ export const AddToPlaylistContextModal = ({ })} searchable size="md" + dropdownOpened={isDropdownOpened} {...form.getInputProps('playlistId')} + onClick={() => setIsDropdownOpened(true)} + onChange={(e) => { + setIsDropdownOpened(false); + form.getInputProps('playlistId').onChange(e); + }} /> <Switch label={t('form.addToPlaylist.input', { diff --git a/src/renderer/features/playlists/components/create-playlist-form.tsx b/src/renderer/features/playlists/components/create-playlist-form.tsx index 046476ca..01bcac38 100644 --- a/src/renderer/features/playlists/components/create-playlist-form.tsx +++ b/src/renderer/features/playlists/components/create-playlist-form.tsx @@ -155,10 +155,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => { )} <Group justify="flex-end"> - <Button - onClick={onCancel} - variant="subtle" - > + <Button onClick={onCancel} variant="subtle"> {t('common.cancel', { postProcess: 'titleCase' })} </Button> <Button diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index c0e9e186..8caedb55 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -331,11 +331,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai /> </VirtualGridAutoSizerContainer> {isPaginationEnabled && ( - <AnimatePresence - initial={false} - mode="wait" - presenceAffectsLayout - > + <AnimatePresence initial={false} mode="wait" presenceAffectsLayout> {page.display === ListDisplayType.TABLE_PAGINATED && ( <TablePagination pageKey={playlistId} diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx index f9495a5c..8b1c7e0f 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx @@ -469,11 +469,7 @@ export const PlaylistDetailSongListHeaderFilters = ({ return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button @@ -555,10 +551,7 @@ export const PlaylistDetailSongListHeaderFilters = ({ {server?.type === ServerType.NAVIDROME && !isSmartPlaylist && ( <> <DropdownMenu.Divider /> - <DropdownMenu.Item - isDanger - onClick={handleToggleShowQueryBuilder} - > + <DropdownMenu.Item isDanger onClick={handleToggleShowQueryBuilder}> {t('action.toggleSmartPlaylistEditor', { postProcess: 'sentenceCase', })} diff --git a/src/renderer/features/playlists/components/playlist-list-content.tsx b/src/renderer/features/playlists/components/playlist-list-content.tsx index d1af6f1a..75940c26 100644 --- a/src/renderer/features/playlists/components/playlist-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-list-content.tsx @@ -33,15 +33,9 @@ export const PlaylistListContent = ({ gridRef, itemCount, tableRef }: PlaylistLi return ( <Suspense fallback={<Spinner container />}> {display === ListDisplayType.CARD || display === ListDisplayType.GRID ? ( - <PlaylistListGridView - gridRef={gridRef} - itemCount={itemCount} - /> + <PlaylistListGridView gridRef={gridRef} itemCount={itemCount} /> ) : ( - <PlaylistListTableView - itemCount={itemCount} - tableRef={tableRef} - /> + <PlaylistListTableView itemCount={itemCount} tableRef={tableRef} /> )} <div /> </Suspense> diff --git a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx index 094415cf..a27a7e8e 100644 --- a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx +++ b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx @@ -355,11 +355,7 @@ export const PlaylistListHeaderFilters = ({ return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button variant="subtle">{sortByLabel}</Button> @@ -378,10 +374,7 @@ export const PlaylistListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> <Divider orientation="vertical" /> - <OrderToggleButton - onToggle={handleToggleSortOrder} - sortOrder={filter.sortOrder} - /> + <OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} /> <RefreshButton onClick={handleRefresh} /> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> @@ -397,14 +390,8 @@ export const PlaylistListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> </Group> - <Group - gap="xs" - wrap="nowrap" - > - <Button - onClick={handleCreatePlaylistModal} - variant="subtle" - > + <Group gap="xs" wrap="nowrap"> + <Button onClick={handleCreatePlaylistModal} variant="subtle"> {t('action.createPlaylist', { postProcess: 'sentenceCase' })} </Button> <ListConfigMenu diff --git a/src/renderer/features/playlists/components/playlist-list-header.tsx b/src/renderer/features/playlists/components/playlist-list-header.tsx index c7d96bac..73ccf598 100644 --- a/src/renderer/features/playlists/components/playlist-list-header.tsx +++ b/src/renderer/features/playlists/components/playlist-list-header.tsx @@ -44,16 +44,9 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis }, 500); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader> - <Flex - align="center" - justify="space-between" - w="100%" - > + <Flex align="center" justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.Title> {t('page.playlistList.title', { postProcess: 'titleCase' })} @@ -67,18 +60,12 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis </Badge> </LibraryHeaderBar> <Group> - <SearchInput - defaultValue={filter.searchTerm} - onChange={handleSearch} - /> + <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} /> </Group> </Flex> </PageHeader> <FilterBar> - <PlaylistListHeaderFilters - gridRef={gridRef} - tableRef={tableRef} - /> + <PlaylistListHeaderFilters gridRef={gridRef} tableRef={tableRef} /> </FilterBar> </Stack> ); diff --git a/src/renderer/features/playlists/components/playlist-query-builder.tsx b/src/renderer/features/playlists/components/playlist-query-builder.tsx index 39927af5..c34ec793 100644 --- a/src/renderer/features/playlists/components/playlist-query-builder.tsx +++ b/src/renderer/features/playlists/components/playlist-query-builder.tsx @@ -410,11 +410,7 @@ export const PlaylistQueryBuilder = forwardRef( ]; return ( - <Flex - direction="column" - h="calc(100% - 2rem)" - justify="space-between" - > + <Flex direction="column" h="calc(100% - 2rem)" justify="space-between"> <ScrollArea> <QueryBuilder data={filters} @@ -442,17 +438,8 @@ export const PlaylistQueryBuilder = forwardRef( uniqueId={filters.uniqueId} /> </ScrollArea> - <Group - align="flex-end" - justify="space-between" - m="1rem" - wrap="nowrap" - > - <Group - gap="sm" - w="100%" - wrap="nowrap" - > + <Group align="flex-end" justify="space-between" m="1rem" wrap="nowrap"> + <Group gap="sm" w="100%" wrap="nowrap"> <Select data={sortOptions} label="Sort" @@ -485,20 +472,11 @@ export const PlaylistQueryBuilder = forwardRef( /> </Group> {onSave && onSaveAs && ( - <Group - gap="sm" - wrap="nowrap" - > - <Button - loading={isSaving} - onClick={handleSaveAs} - > + <Group gap="sm" wrap="nowrap"> + <Button loading={isSaving} onClick={handleSaveAs}> {t('common.saveAs', { postProcess: 'titleCase' })} </Button> - <Button - onClick={openPreviewModal} - variant="subtle" - > + <Button onClick={openPreviewModal} variant="subtle"> {t('common.preview', { postProcess: 'titleCase' })} </Button> <DropdownMenu position="bottom-end"> @@ -512,12 +490,7 @@ export const PlaylistQueryBuilder = forwardRef( <DropdownMenu.Dropdown> <DropdownMenu.Item isDanger - leftSection={ - <Icon - color="error" - icon="save" - /> - } + leftSection={<Icon color="error" icon="save" />} onClick={handleSave} > {t('common.saveAndReplace', { postProcess: 'titleCase' })} diff --git a/src/renderer/features/playlists/components/save-as-playlist-form.tsx b/src/renderer/features/playlists/components/save-as-playlist-form.tsx index 945f6a30..304b1a8f 100644 --- a/src/renderer/features/playlists/components/save-as-playlist-form.tsx +++ b/src/renderer/features/playlists/components/save-as-playlist-form.tsx @@ -103,10 +103,7 @@ export const SaveAsPlaylistForm = ({ /> )} <Group justify="flex-end"> - <Button - onClick={onCancel} - variant="subtle" - > + <Button onClick={onCancel} variant="subtle"> {t('common.cancel', { postProcess: 'titleCase' })} </Button> <Button diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index 98723b56..51635c52 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -140,10 +140,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl </> )} <Group justify="flex-end"> - <Button - onClick={onCancel} - variant="subtle" - > + <Button onClick={onCancel} variant="subtle"> {t('common.cancel', { postProcess: 'titleCase' })} </Button> <Button diff --git a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx index 7d006dda..09a24346 100644 --- a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx @@ -175,12 +175,7 @@ const PlaylistDetailSongListRoute = () => { {(isSmartPlaylist || showQueryBuilder) && ( <motion.div> - <Box - h="100%" - mah="35vh" - p="md" - w="100%" - > + <Box h="100%" mah="35vh" p="md" w="100%"> <Group pb="md"> <ActionIcon icon={isQueryBuilderExpanded ? 'arrowUpS' : 'arrowDownS'} diff --git a/src/renderer/features/playlists/routes/playlist-list-route.tsx b/src/renderer/features/playlists/routes/playlist-list-route.tsx index e7a332d9..fc81f77f 100644 --- a/src/renderer/features/playlists/routes/playlist-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-list-route.tsx @@ -50,16 +50,8 @@ const PlaylistListRoute = () => { return ( <AnimatedPage> <ListContext.Provider value={providerValue}> - <PlaylistListHeader - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> - <PlaylistListContent - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> + <PlaylistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> + <PlaylistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> </ListContext.Provider> </AnimatedPage> ); diff --git a/src/renderer/features/search/components/command-item-selectable.tsx b/src/renderer/features/search/components/command-item-selectable.tsx new file mode 100644 index 00000000..91e63740 --- /dev/null +++ b/src/renderer/features/search/components/command-item-selectable.tsx @@ -0,0 +1,37 @@ +import { Command } from 'cmdk'; +import { ComponentPropsWithoutRef, ReactNode, useEffect, useRef, useState } from 'react'; + +interface CommandItemSelectableProps + extends Omit<ComponentPropsWithoutRef<typeof Command.Item>, 'children'> { + children: (args: { isHighlighted: boolean }) => ReactNode; +} + +export function CommandItemSelectable({ children, ...itemProps }: CommandItemSelectableProps) { + const ref = useRef<HTMLDivElement>(null); + const [isHighlighted, setIsHighlighted] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + setIsHighlighted(el.getAttribute('aria-selected') === 'true'); + + const observer = new MutationObserver(() => { + const selected = el.getAttribute('aria-selected') === 'true'; + setIsHighlighted(selected); + }); + + observer.observe(el, { + attributeFilter: ['aria-selected'], + attributes: true, + }); + + return () => observer.disconnect(); + }, []); + + return ( + <Command.Item {...itemProps} ref={ref}> + {children({ isHighlighted })} + </Command.Item> + ); +} diff --git a/src/renderer/features/search/components/command-palette.tsx b/src/renderer/features/search/components/command-palette.tsx index ad0da392..bef4fd41 100644 --- a/src/renderer/features/search/components/command-palette.tsx +++ b/src/renderer/features/search/components/command-palette.tsx @@ -5,6 +5,7 @@ import { generatePath, useNavigate } from 'react-router'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; +import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands'; import { HomeCommands } from '/@/renderer/features/search/components/home-commands'; import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; @@ -95,18 +96,11 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { header: { display: 'none' }, }} > - <Group - gap="sm" - mb="1rem" - > + <Group gap="sm" mb="1rem"> {pages.map((page, index) => ( <Fragment key={page}> {index > 0 && ' > '} - <Button - disabled - size="compact-md" - variant="default" - > + <Button disabled size="compact-md" variant="default"> {page?.toLocaleUpperCase()} </Button> </Fragment> @@ -119,6 +113,13 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { return 0; }} label="Global Command Menu" + onKeyDown={(e) => { + // Focus the search input when navigating with arrow keys + // to prevent the focus from staying on the command-item ActionIcon + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + searchInputRef.current?.focus(); + } + }} onValueChange={setValue} value={value} > @@ -149,7 +150,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { {showAlbumGroup && ( <Command.Group heading="Albums"> {data?.albums?.map((album) => ( - <Command.Item + <CommandItemSelectable key={`search-album-${album.id}`} onSelect={() => { navigate( @@ -162,24 +163,27 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { }} value={`search-${album.id}`} > - <LibraryCommandItem - handlePlayQueueAdd={handlePlayQueueAdd} - id={album.id} - imageUrl={album.imageUrl} - itemType={LibraryItem.ALBUM} - subtitle={album.albumArtists - .map((artist) => artist.name) - .join(', ')} - title={album.name} - /> - </Command.Item> + {({ isHighlighted }) => ( + <LibraryCommandItem + handlePlayQueueAdd={handlePlayQueueAdd} + id={album.id} + imageUrl={album.imageUrl} + isHighlighted={isHighlighted} + itemType={LibraryItem.ALBUM} + subtitle={album.albumArtists + .map((artist) => artist.name) + .join(', ')} + title={album.name} + /> + )} + </CommandItemSelectable> ))} </Command.Group> )} {showArtistGroup && ( <Command.Group heading="Artists"> {data?.albumArtists.map((artist) => ( - <Command.Item + <CommandItemSelectable key={`artist-${artist.id}`} onSelect={() => { navigate( @@ -192,30 +196,33 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { }} value={`search-${artist.id}`} > - <LibraryCommandItem - disabled={artist?.albumCount === 0} - handlePlayQueueAdd={handlePlayQueueAdd} - id={artist.id} - imageUrl={artist.imageUrl} - itemType={LibraryItem.ALBUM_ARTIST} - subtitle={ - artist?.albumCount !== undefined && - artist?.albumCount !== null - ? t('entity.albumWithCount', { - count: artist.albumCount, - }) - : undefined - } - title={artist.name} - /> - </Command.Item> + {({ isHighlighted }) => ( + <LibraryCommandItem + disabled={artist?.albumCount === 0} + handlePlayQueueAdd={handlePlayQueueAdd} + id={artist.id} + imageUrl={artist.imageUrl} + isHighlighted={isHighlighted} + itemType={LibraryItem.ALBUM_ARTIST} + subtitle={ + artist?.albumCount !== undefined && + artist?.albumCount !== null + ? t('entity.albumWithCount', { + count: artist.albumCount, + }) + : undefined + } + title={artist.name} + /> + )} + </CommandItemSelectable> ))} </Command.Group> )} {showTrackGroup && ( <Command.Group heading="Tracks"> {data?.songs.map((song) => ( - <Command.Item + <CommandItemSelectable key={`artist-${song.id}`} onSelect={() => { navigate( @@ -228,17 +235,20 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { }} value={`search-${song.id}`} > - <LibraryCommandItem - handlePlayQueueAdd={handlePlayQueueAdd} - id={song.id} - imageUrl={song.imageUrl} - itemType={LibraryItem.SONG} - subtitle={song.artists - .map((artist) => artist.name) - .join(', ')} - title={song.name} - /> - </Command.Item> + {({ isHighlighted }) => ( + <LibraryCommandItem + handlePlayQueueAdd={handlePlayQueueAdd} + id={song.id} + imageUrl={song.imageUrl} + isHighlighted={isHighlighted} + itemType={LibraryItem.SONG} + subtitle={song.artists + .map((artist) => artist.name) + .join(', ')} + title={song.name} + /> + )} + </CommandItemSelectable> ))} </Command.Group> )} @@ -267,10 +277,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { )} </Command.List> </Command> - <Box - mt="0.5rem" - p="0.5rem" - > + <Box mt="0.5rem" p="0.5rem"> <Group justify="space-between"> <Command.Loading> {isHome && isLoading && query !== '' && <Spinner />} diff --git a/src/renderer/features/search/components/library-command-item.tsx b/src/renderer/features/search/components/library-command-item.tsx index 4c44a1b5..aa275315 100644 --- a/src/renderer/features/search/components/library-command-item.tsx +++ b/src/renderer/features/search/components/library-command-item.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, MouseEvent, useCallback, useState } from 'react'; +import { CSSProperties, SyntheticEvent, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './library-command-item.module.css'; @@ -16,6 +16,7 @@ interface LibraryCommandItemProps { handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void; id: string; imageUrl: null | string; + isHighlighted?: boolean; itemType: LibraryItem; subtitle?: string; title?: string; @@ -26,6 +27,7 @@ export const LibraryCommandItem = ({ handlePlayQueueAdd, id, imageUrl, + isHighlighted, itemType, subtitle, title, @@ -33,8 +35,9 @@ export const LibraryCommandItem = ({ const { t } = useTranslation(); const handlePlay = useCallback( - (e: MouseEvent, id: string, playType: Play) => { + (e: SyntheticEvent, id: string, playType: Play) => { e.stopPropagation(); + e.preventDefault(); handlePlayQueueAdd?.({ byItemType: { id: [id], @@ -48,6 +51,8 @@ export const LibraryCommandItem = ({ const [isHovered, setIsHovered] = useState(false); + const showControls = isHighlighted || isHovered; + return ( <Flex gap="xl" @@ -56,10 +61,7 @@ export const LibraryCommandItem = ({ onMouseLeave={() => setIsHovered(false)} style={{ height: '40px', width: '100%' }} > - <div - className={styles.itemGrid} - style={{ '--item-height': '40px' } as CSSProperties} - > + <div className={styles.itemGrid} style={{ '--item-height': '40px' } as CSSProperties}> <div className={styles.imageWrapper}> <Image alt="cover" @@ -71,26 +73,24 @@ export const LibraryCommandItem = ({ </div> <div className={styles.metadataWrapper}> <Text overflow="hidden">{title}</Text> - <Text - isMuted - overflow="hidden" - > + <Text isMuted overflow="hidden"> {subtitle} </Text> </div> </div> - {isHovered && ( - <Group - align="center" - gap="sm" - justify="flex-end" - wrap="nowrap" - > + {showControls && ( + <Group align="center" gap="sm" justify="flex-end" wrap="nowrap"> <ActionIcon disabled={disabled} icon="mediaPlay" onClick={(e) => handlePlay(e, id, Play.NOW)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handlePlay(e, id, Play.NOW); + } + }} size="xs" + tabIndex={disabled ? -1 : 0} tooltip={{ label: t('player.play', { postProcess: 'sentenceCase' }), openDelay: 500, @@ -102,7 +102,13 @@ export const LibraryCommandItem = ({ disabled={disabled} icon="mediaShuffle" onClick={(e) => handlePlay(e, id, Play.SHUFFLE)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handlePlay(e, id, Play.SHUFFLE); + } + }} size="xs" + tabIndex={disabled ? -1 : 0} tooltip={{ label: t('player.shuffle', { postProcess: 'sentenceCase' }), openDelay: 500, @@ -114,7 +120,13 @@ export const LibraryCommandItem = ({ disabled={disabled} icon="mediaPlayLast" onClick={(e) => handlePlay(e, id, Play.LAST)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handlePlay(e, id, Play.LAST); + } + }} size="xs" + tabIndex={disabled ? -1 : 0} tooltip={{ label: t('player.addLast', { postProcess: 'sentenceCase' }), @@ -126,7 +138,13 @@ export const LibraryCommandItem = ({ disabled={disabled} icon="mediaPlayNext" onClick={(e) => handlePlay(e, id, Play.NEXT)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handlePlay(e, id, Play.NEXT); + } + }} size="xs" + tabIndex={disabled ? -1 : 0} tooltip={{ label: t('player.addNext', { postProcess: 'sentenceCase' }), openDelay: 500, diff --git a/src/renderer/features/search/components/search-header.tsx b/src/renderer/features/search/components/search-header.tsx index b24f2d01..8f62cbbe 100644 --- a/src/renderer/features/search/components/search-header.tsx +++ b/src/renderer/features/search/components/search-header.tsx @@ -49,15 +49,9 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => { }, 200); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader> - <Flex - justify="space-between" - w="100%" - > + <Flex justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.Title>Search</LibraryHeaderBar.Title> </LibraryHeaderBar> diff --git a/src/renderer/features/search/routes/search-route.tsx b/src/renderer/features/search/routes/search-route.tsx index 8d620282..96ae5c67 100644 --- a/src/renderer/features/search/routes/search-route.tsx +++ b/src/renderer/features/search/routes/search-route.tsx @@ -16,14 +16,8 @@ const SearchRoute = () => { return ( <AnimatedPage key={`search-${navigationId}`}> - <SearchHeader - navigationId={navigationId} - tableRef={tableRef} - /> - <SearchContent - key={`page-${itemType}`} - tableRef={tableRef} - /> + <SearchHeader navigationId={navigationId} tableRef={tableRef} /> + <SearchContent key={`page-${itemType}`} tableRef={tableRef} /> </AnimatedPage> ); }; diff --git a/src/renderer/features/servers/components/add-server-form.tsx b/src/renderer/features/servers/components/add-server-form.tsx index bb4d8916..c9768e0d 100644 --- a/src/renderer/features/servers/components/add-server-form.tsx +++ b/src/renderer/features/servers/components/add-server-form.tsx @@ -31,15 +31,8 @@ interface AddServerFormProps { function ServerIconWithLabel({ icon, label }: { icon: string; label: string }) { return ( - <Stack - align="center" - justify="center" - > - <img - height="50" - src={icon} - width="50" - /> + <Stack align="center" justify="center"> + <img height="50" src={icon} width="50" /> <Text>{label}</Text> </Stack> ); @@ -47,30 +40,15 @@ function ServerIconWithLabel({ icon, label }: { icon: string; label: string }) { const SERVER_TYPES = [ { - label: ( - <ServerIconWithLabel - icon={JellyfinIcon} - label="Jellyfin" - /> - ), + label: <ServerIconWithLabel icon={JellyfinIcon} label="Jellyfin" />, value: ServerType.JELLYFIN, }, { - label: ( - <ServerIconWithLabel - icon={NavidromeIcon} - label="Navidrome" - /> - ), + label: <ServerIconWithLabel icon={NavidromeIcon} label="Navidrome" />, value: ServerType.NAVIDROME, }, { - label: ( - <ServerIconWithLabel - icon={SubsonicIcon} - label="OpenSubsonic" - /> - ), + label: <ServerIconWithLabel icon={SubsonicIcon} label="OpenSubsonic" />, value: ServerType.SUBSONIC, }, ]; @@ -174,10 +152,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { return ( <form onSubmit={handleSubmit}> - <Stack - m={5} - ref={focusTrapRef} - > + <Stack m={5} ref={focusTrapRef}> <SegmentedControl data={SERVER_TYPES} disabled={Boolean(serverLock)} @@ -238,15 +213,9 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { {...form.getInputProps('legacyAuth', { type: 'checkbox' })} /> )} - <Group - grow - justify="flex-end" - > + <Group grow justify="flex-end"> {onCancel && ( - <Button - onClick={onCancel} - variant="subtle" - > + <Button onClick={onCancel} variant="subtle"> {t('common.cancel', { postProcess: 'titleCase' })} </Button> )} diff --git a/src/renderer/features/servers/components/edit-server-form.tsx b/src/renderer/features/servers/components/edit-server-form.tsx index bd547fa9..8765b33e 100644 --- a/src/renderer/features/servers/components/edit-server-form.tsx +++ b/src/renderer/features/servers/components/edit-server-form.tsx @@ -33,10 +33,7 @@ interface EditServerFormProps { const ModifiedFieldIndicator = () => { return ( <Tooltip label={i18n.t('common.modified', { postProcess: 'titleCase' }) as string}> - <Icon - color="warn" - icon="info" - /> + <Icon color="warn" icon="info" /> </Tooltip> ); }; @@ -193,17 +190,10 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer /> )} <Group justify="flex-end"> - <Button - onClick={onCancel} - variant="subtle" - > + <Button onClick={onCancel} variant="subtle"> {t('common.cancel', { postProcess: 'titleCase' })} </Button> - <Button - loading={isLoading} - type="submit" - variant="filled" - > + <Button loading={isLoading} type="submit" variant="filled"> {t('common.save', { postProcess: 'titleCase' })} </Button> </Group> diff --git a/src/renderer/features/servers/components/server-list-item.tsx b/src/renderer/features/servers/components/server-list-item.tsx index 134ae61e..112263c1 100644 --- a/src/renderer/features/servers/components/server-list-item.tsx +++ b/src/renderer/features/servers/components/server-list-item.tsx @@ -66,11 +66,7 @@ export const ServerListItem = ({ server }: ServerListItemProps) => { /> ) : ( <Stack> - <Table - layout="fixed" - variant="vertical" - withTableBorder - > + <Table layout="fixed" variant="vertical" withTableBorder> <Table.Tbody> <Table.Tr> <Table.Th> diff --git a/src/renderer/features/servers/components/server-list.tsx b/src/renderer/features/servers/components/server-list.tsx index f88e2b44..0e1423a0 100644 --- a/src/renderer/features/servers/components/server-list.tsx +++ b/src/renderer/features/servers/components/server-list.tsx @@ -73,10 +73,7 @@ export const ServerList = () => { {Object.keys(serverListQuery)?.map((serverId) => { const server = serverListQuery[serverId]; return ( - <Accordion.Item - key={server.id} - value={server.name} - > + <Accordion.Item key={server.id} value={server.name}> <Accordion.Control> <Group> <img @@ -103,10 +100,7 @@ export const ServerList = () => { </Accordion.Item> ); })} - <Group - grow - pt="md" - > + <Group grow pt="md"> <Button autoFocus leftSection={<Icon icon="add" />} diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index c9c8ee7d..20039d1d 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -22,6 +22,8 @@ import { FontType } from '/@/shared/types/types'; const localSettings = isElectron() ? window.api.localSettings : null; const ipc = isElectron() ? window.api.ipc : null; +// Electron 32+ removed file.path, use this which is exposed in preload to get real path +const webUtils = isElectron() ? window.electron.webUtils : null; type Font = { label: string; @@ -165,7 +167,10 @@ export const ApplicationSettings = () => { { control: ( <Select - data={languages} + data={languages.map((language) => ({ + label: `${language.label} (${language.value})`, + value: language.value, + }))} onChange={handleChangeLanguage} value={settings.language} /> @@ -251,7 +256,7 @@ export const ApplicationSettings = () => { setSettings({ font: { ...fontSettings, - custom: e?.path ?? null, + custom: e ? webUtils?.getPathForFile(e) || null : null, }, }) } diff --git a/src/renderer/features/settings/components/general/context-menu-settings.tsx b/src/renderer/features/settings/components/general/context-menu-settings.tsx index 30e9f824..7d3768bb 100644 --- a/src/renderer/features/settings/components/general/context-menu-settings.tsx +++ b/src/renderer/features/settings/components/general/context-menu-settings.tsx @@ -22,11 +22,7 @@ export const ContextMenuSettings = () => { <> <SettingsOptions control={ - <Button - onClick={() => setOpen(!open)} - size="compact-md" - variant="filled" - > + <Button onClick={() => setOpen(!open)} size="compact-md" variant="filled"> {t(open ? 'common.close' : 'common.edit', { postProcess: 'titleCase' })} </Button> } diff --git a/src/renderer/features/settings/components/general/draggable-item.tsx b/src/renderer/features/settings/components/general/draggable-item.tsx index 5879ce49..1a13d47c 100644 --- a/src/renderer/features/settings/components/general/draggable-item.tsx +++ b/src/renderer/features/settings/components/general/draggable-item.tsx @@ -35,17 +35,8 @@ export const DraggableItem = ({ handleChangeDisabled, item, value }: DraggableIt const dragControls = useDragControls(); return ( - <Reorder.Item - as="div" - dragControls={dragControls} - dragListener={false} - value={item} - > - <Group - py="md" - style={{ boxShadow: '0 1px 3px rgba(0,0,0,.1)' }} - wrap="nowrap" - > + <Reorder.Item as="div" dragControls={dragControls} dragListener={false} value={item}> + <Group py="md" style={{ boxShadow: '0 1px 3px rgba(0,0,0,.1)' }} wrap="nowrap"> <Checkbox checked={!item.disabled} onChange={(e) => handleChangeDisabled(item.id, e.target.checked)} diff --git a/src/renderer/features/settings/components/general/remote-settings.tsx b/src/renderer/features/settings/components/general/remote-settings.tsx index a982e7c1..828c2719 100644 --- a/src/renderer/features/settings/components/general/remote-settings.tsx +++ b/src/renderer/features/settings/components/general/remote-settings.tsx @@ -73,20 +73,12 @@ export const RemoteSettings = () => { /> ), description: ( - <Text - isMuted - isNoSelect - size="sm" - > + <Text isMuted isNoSelect size="sm"> {t('setting.enableRemote', { context: 'description', postProcess: 'sentenceCase', })}{' '} - <a - href={url} - rel="noreferrer noopener" - target="_blank" - > + <a href={url} rel="noreferrer noopener" target="_blank"> {url} </a> </Text> diff --git a/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx b/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx index 0b3eddb0..1a126569 100644 --- a/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx +++ b/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx @@ -240,10 +240,7 @@ export const HotkeyManagerSettings = () => { /> <div className={styles.container}> {filteredBindings.map((binding) => ( - <Group - key={`hotkey-${binding}`} - wrap="nowrap" - > + <Group key={`hotkey-${binding}`} wrap="nowrap"> <TextInput readOnly style={{ userSelect: 'none' }} diff --git a/src/renderer/features/settings/components/playback/audio-settings.tsx b/src/renderer/features/settings/components/playback/audio-settings.tsx index b3fdb54c..c17fab7a 100644 --- a/src/renderer/features/settings/components/playback/audio-settings.tsx +++ b/src/renderer/features/settings/components/playback/audio-settings.tsx @@ -244,10 +244,5 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) => }, ]; - return ( - <SettingsSection - divider={!hasFancyAudio} - options={audioOptions} - /> - ); + return <SettingsSection divider={!hasFancyAudio} options={audioOptions} />; }; diff --git a/src/renderer/features/settings/components/playback/lyric-settings.tsx b/src/renderer/features/settings/components/playback/lyric-settings.tsx index 972f7d46..30fc8d45 100644 --- a/src/renderer/features/settings/components/playback/lyric-settings.tsx +++ b/src/renderer/features/settings/components/playback/lyric-settings.tsx @@ -215,10 +215,5 @@ export const LyricSettings = () => { }, ]; - return ( - <SettingsSection - divider={false} - options={lyricOptions} - /> - ); + return <SettingsSection divider={false} options={lyricOptions} />; }; diff --git a/src/renderer/features/settings/components/playback/mpv-settings.tsx b/src/renderer/features/settings/components/playback/mpv-settings.tsx index 55f518cb..354e07d7 100644 --- a/src/renderer/features/settings/components/playback/mpv-settings.tsx +++ b/src/renderer/features/settings/components/playback/mpv-settings.tsx @@ -218,11 +218,7 @@ export const MpvSettings = () => { ), description: ( <Stack gap={0}> - <Text - isMuted - isNoSelect - size="sm" - > + <Text isMuted isNoSelect size="sm"> {t('setting.mpvExtraParameters', { context: 'description', postProcess: 'sentenceCase', diff --git a/src/renderer/features/settings/components/playback/transcode-settings.tsx b/src/renderer/features/settings/components/playback/transcode-settings.tsx index bc8a8843..080cb54a 100644 --- a/src/renderer/features/settings/components/playback/transcode-settings.tsx +++ b/src/renderer/features/settings/components/playback/transcode-settings.tsx @@ -86,10 +86,5 @@ export const TranscodeSettings = () => { }, ]; - return ( - <SettingsSection - divider - options={transcodeOptions} - /> - ); + return <SettingsSection divider options={transcodeOptions} />; }; diff --git a/src/renderer/features/settings/components/settings-header.tsx b/src/renderer/features/settings/components/settings-header.tsx index 4ca8c1d5..ecf24145 100644 --- a/src/renderer/features/settings/components/settings-header.tsx +++ b/src/renderer/features/settings/components/settings-header.tsx @@ -44,16 +44,9 @@ export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => { <Flex ref={cq.ref}> <PageHeader> <LibraryHeaderBar> - <Flex - align="center" - justify="space-between" - w="100%" - > + <Flex align="center" justify="space-between" w="100%"> <Group wrap="nowrap"> - <Icon - icon="settings" - size="5xl" - /> + <Icon icon="settings" size="5xl" /> <LibraryHeaderBar.Title> {t('common.setting', { count: 2, postProcess: 'titleCase' })} </LibraryHeaderBar.Title> @@ -65,10 +58,7 @@ export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => { setSearch(event.target.value.toLocaleLowerCase()) } /> - <Button - onClick={openResetConfirmModal} - variant="default" - > + <Button onClick={openResetConfirmModal} variant="default"> {t('common.resetToDefault', { postProcess: 'sentenceCase' })} </Button> </Group> diff --git a/src/renderer/features/settings/components/settings-option.tsx b/src/renderer/features/settings/components/settings-option.tsx index 545b2881..85b7b890 100644 --- a/src/renderer/features/settings/components/settings-option.tsx +++ b/src/renderer/features/settings/components/settings-option.tsx @@ -16,11 +16,7 @@ interface SettingsOptionProps { export const SettingsOptions = ({ control, description, note, title }: SettingsOptionProps) => { return ( <> - <Group - justify="space-between" - style={{ alignItems: 'center' }} - wrap="nowrap" - > + <Group justify="space-between" style={{ alignItems: 'center' }} wrap="nowrap"> <Stack gap="xs" style={{ @@ -30,17 +26,11 @@ export const SettingsOptions = ({ control, description, note, title }: SettingsO }} > <Group> - <Text - isNoSelect - size="md" - > + <Text isNoSelect size="md"> {title} </Text> {note && ( - <Tooltip - label={note} - openDelay={0} - > + <Tooltip label={note} openDelay={0}> <Icon icon="info" /> </Tooltip> )} @@ -48,11 +38,7 @@ export const SettingsOptions = ({ control, description, note, title }: SettingsO {React.isValidElement(description) ? ( description ) : ( - <Text - isMuted - isNoSelect - size="sm" - > + <Text isMuted isNoSelect size="sm"> {description} </Text> )} diff --git a/src/renderer/features/settings/components/settings-section.tsx b/src/renderer/features/settings/components/settings-section.tsx index 32441a58..0e555976 100644 --- a/src/renderer/features/settings/components/settings-section.tsx +++ b/src/renderer/features/settings/components/settings-section.tsx @@ -28,10 +28,7 @@ export const SettingsSection = ({ divider, options }: SettingsSectionProps) => { return ( <> {values.map((option) => ( - <SettingsOptions - key={`option-${option.title}`} - {...option} - /> + <SettingsOptions key={`option-${option.title}`} {...option} /> ))} {divider !== false && values.length > 0 && <Divider />} </> diff --git a/src/renderer/features/settings/components/window/cache-settngs.tsx b/src/renderer/features/settings/components/window/cache-settngs.tsx index a32138da..23d2e1f2 100644 --- a/src/renderer/features/settings/components/window/cache-settngs.tsx +++ b/src/renderer/features/settings/components/window/cache-settngs.tsx @@ -94,10 +94,5 @@ export const CacheSettings = () => { }, ]; - return ( - <SettingsSection - divider={false} - options={options} - /> - ); + return <SettingsSection divider={false} options={options} />; }; diff --git a/src/renderer/features/settings/components/window/discord-settings.tsx b/src/renderer/features/settings/components/window/discord-settings.tsx index af8ce176..1cad180c 100644 --- a/src/renderer/features/settings/components/window/discord-settings.tsx +++ b/src/renderer/features/settings/components/window/discord-settings.tsx @@ -6,10 +6,12 @@ import { SettingsSection, } from '/@/renderer/features/settings/components/settings-section'; import { + DiscordDisplayType, useDiscordSetttings, useGeneralSettings, useSettingsStoreActions, } from '/@/renderer/store'; +import { Select } from '/@/shared/components/select/select'; import { Switch } from '/@/shared/components/switch/switch'; import { TextInput } from '/@/shared/components/text-input/text-input'; @@ -120,6 +122,50 @@ export const DiscordSettings = () => { postProcess: 'sentenceCase', }), }, + { + control: ( + <Select + aria-label={t('setting.discordDisplayType')} + clearable={false} + data={[ + { label: 'Feishin', value: DiscordDisplayType.FEISHIN }, + { + label: t('setting.discordDisplayType', { + context: 'songname', + postProcess: 'sentenceCase', + }), + value: DiscordDisplayType.SONG_NAME, + }, + { + label: t('setting.discordDisplayType_artistname', { + context: 'artistname', + postProcess: 'sentenceCase', + }), + value: DiscordDisplayType.ARTIST_NAME, + }, + ]} + defaultValue={settings.displayType} + onChange={(e) => { + if (!e) return; + setSettings({ + discord: { + ...settings, + displayType: e as DiscordDisplayType, + }, + }); + }} + /> + ), + description: t('setting.discordDisplayType', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.discordDisplayType', { + discord: 'Discord', + postProcess: 'sentenceCase', + }), + }, { control: ( <Switch diff --git a/src/renderer/features/settings/components/window/password-settings.tsx b/src/renderer/features/settings/components/window/password-settings.tsx index cfe76611..e01b79ad 100644 --- a/src/renderer/features/settings/components/window/password-settings.tsx +++ b/src/renderer/features/settings/components/window/password-settings.tsx @@ -52,10 +52,5 @@ export const PasswordSettings = () => { }, ]; - return ( - <SettingsSection - divider={false} - options={updateOptions} - /> - ); + return <SettingsSection divider={false} options={updateOptions} />; }; diff --git a/src/renderer/features/settings/components/window/update-settings.tsx b/src/renderer/features/settings/components/window/update-settings.tsx index f090a570..6a0bf4fc 100644 --- a/src/renderer/features/settings/components/window/update-settings.tsx +++ b/src/renderer/features/settings/components/window/update-settings.tsx @@ -44,10 +44,5 @@ export const UpdateSettings = () => { }, ]; - return ( - <SettingsSection - divider={utils?.isLinux()} - options={updateOptions} - /> - ); + return <SettingsSection divider={utils?.isLinux()} options={updateOptions} />; }; diff --git a/src/renderer/features/settings/routes/settings-route.tsx b/src/renderer/features/settings/routes/settings-route.tsx index 3d25f6d7..4255b225 100644 --- a/src/renderer/features/settings/routes/settings-route.tsx +++ b/src/renderer/features/settings/routes/settings-route.tsx @@ -12,11 +12,7 @@ const SettingsRoute = () => { return ( <AnimatedPage> <SettingSearchContext.Provider value={search}> - <Flex - direction="column" - h="100%" - w="100%" - > + <Flex direction="column" h="100%" w="100%"> <SettingsHeader setSearch={setSearch} /> <SettingsContent /> </Flex> diff --git a/src/renderer/features/shared/components/animated-page.tsx b/src/renderer/features/shared/components/animated-page.tsx index dfac2095..54d0e56b 100644 --- a/src/renderer/features/shared/components/animated-page.tsx +++ b/src/renderer/features/shared/components/animated-page.tsx @@ -14,11 +14,7 @@ interface AnimatedPageProps { export const AnimatedPage = forwardRef( ({ children }: AnimatedPageProps, ref: Ref<HTMLDivElement>) => { return ( - <motion.main - className={styles.animatedPage} - ref={ref} - {...animationProps.fadeIn} - > + <motion.main className={styles.animatedPage} ref={ref} {...animationProps.fadeIn}> {children} </motion.main> ); diff --git a/src/renderer/features/shared/components/filter-bar.tsx b/src/renderer/features/shared/components/filter-bar.tsx index d87c0a9d..eeca504c 100644 --- a/src/renderer/features/shared/components/filter-bar.tsx +++ b/src/renderer/features/shared/components/filter-bar.tsx @@ -2,10 +2,7 @@ import styles from './filter-bar.module.css'; export const FilterBar = ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => { return ( - <div - className={styles.filterBar} - {...props} - > + <div className={styles.filterBar} {...props}> {children} </div> ); diff --git a/src/renderer/features/shared/components/library-background-overlay.tsx b/src/renderer/features/shared/components/library-background-overlay.tsx index 66246720..0c0b3010 100644 --- a/src/renderer/features/shared/components/library-background-overlay.tsx +++ b/src/renderer/features/shared/components/library-background-overlay.tsx @@ -5,10 +5,5 @@ interface LibraryBackgroundOverlayProps { } export const LibraryBackgroundOverlay = ({ backgroundColor }: LibraryBackgroundOverlayProps) => { - return ( - <div - className={styles.root} - style={{ backgroundColor }} - /> - ); + return <div className={styles.root} style={{ backgroundColor }} />; }; diff --git a/src/renderer/features/shared/components/library-header-bar.tsx b/src/renderer/features/shared/components/library-header-bar.tsx index 11074723..7eded7ad 100644 --- a/src/renderer/features/shared/components/library-header-bar.tsx +++ b/src/renderer/features/shared/components/library-header-bar.tsx @@ -22,21 +22,14 @@ interface TitleProps { const HeaderPlayButton = ({ className, ...props }: PlayButtonProps) => { return ( <div className={styles.playButtonContainer}> - <PlayButton - className={className} - {...props} - /> + <PlayButton className={className} {...props} /> </div> ); }; const Title = ({ children }: TitleProps) => { return ( - <TextTitle - fw={700} - order={1} - overflow="hidden" - > + <TextTitle fw={700} order={1} overflow="hidden"> {children} </TextTitle> ); diff --git a/src/renderer/features/shared/components/library-header.tsx b/src/renderer/features/shared/components/library-header.tsx index 2b53c2ea..5518f733 100644 --- a/src/renderer/features/shared/components/library-header.tsx +++ b/src/renderer/features/shared/components/library-header.tsx @@ -84,10 +84,7 @@ export const LibraryHeader = forwardRef( }, [imageUrl, isImageError]); return ( - <div - className={styles.libraryHeader} - ref={ref} - > + <div className={styles.libraryHeader} ref={ref}> <div className={styles.background} style={{ background, filter: `blur(${blur ?? 0}rem)` }} @@ -130,10 +127,7 @@ export const LibraryHeader = forwardRef( {itemTypeString()} </Text> <h1 className={styles.title}> - <AutoTextSize - maxFontSizePx={80} - mode="box" - > + <AutoTextSize maxFontSizePx={80} mode="box"> {title} </AutoTextSize> </h1> diff --git a/src/renderer/features/shared/components/list-config-menu.tsx b/src/renderer/features/shared/components/list-config-menu.tsx index 7e448abc..463dc31a 100644 --- a/src/renderer/features/shared/components/list-config-menu.tsx +++ b/src/renderer/features/shared/components/list-config-menu.tsx @@ -16,14 +16,8 @@ import { ListDisplayType } from '/@/shared/types/types'; const DISPLAY_TYPES = [ { label: ( - <Stack - align="center" - p="sm" - > - <Icon - icon="layoutTable" - size="lg" - /> + <Stack align="center" p="sm"> + <Icon icon="layoutTable" size="lg" /> {i18n.t('table.config.view.table', { postProcess: 'sentenceCase' }) as string} </Stack> ), @@ -31,14 +25,8 @@ const DISPLAY_TYPES = [ }, { label: ( - <Stack - align="center" - p="sm" - > - <Icon - icon="layoutGrid" - size="lg" - /> + <Stack align="center" p="sm"> + <Icon icon="layoutGrid" size="lg" /> {i18n.t('table.config.view.card', { postProcess: 'sentenceCase' }) as string} </Stack> ), @@ -47,14 +35,8 @@ const DISPLAY_TYPES = [ { disabled: true, label: ( - <Stack - align="center" - p="sm" - > - <Icon - icon="layoutList" - size="lg" - /> + <Stack align="center" p="sm"> + <Icon icon="layoutList" size="lg" /> {i18n.t('table.config.view.list', { postProcess: 'sentenceCase' }) as string} </Stack> ), @@ -79,10 +61,7 @@ interface ListConfigMenuProps { export const ListConfigMenu = (props: ListConfigMenuProps) => { return ( - <Popover - position="bottom-end" - width={300} - > + <Popover position="bottom-end" width={300}> <Popover.Target> <SettingsButton /> </Popover.Target> @@ -161,12 +140,7 @@ const TableConfig = ({ return ( <> - <Table - variant="vertical" - withColumnBorders - withRowBorders - withTableBorder - > + <Table variant="vertical" withColumnBorders withRowBorders withTableBorder> <Table.Tbody> <Table.Tr> <Table.Th> @@ -199,10 +173,7 @@ const TableConfig = ({ </Table.Tr> </Table.Tbody> </Table> - <ScrollArea - allowDragScroll - style={{ maxHeight: '200px' }} - > + <ScrollArea allowDragScroll style={{ maxHeight: '200px' }}> <CheckboxSelect data={tableColumnsData} onChange={onChangeTableColumns} @@ -227,12 +198,7 @@ const GridConfig = ({ itemSize, onChangeItemGap, onChangeItemSize }: GridConfigP return ( <> - <Table - variant="vertical" - withColumnBorders - withRowBorders - withTableBorder - > + <Table variant="vertical" withColumnBorders withRowBorders withTableBorder> <Table.Tbody> <Table.Tr> <Table.Th w="50%"> diff --git a/src/renderer/features/shared/components/search-input.tsx b/src/renderer/features/shared/components/search-input.tsx index c9980d7c..75a1b2ce 100644 --- a/src/renderer/features/shared/components/search-input.tsx +++ b/src/renderer/features/shared/components/search-input.tsx @@ -46,11 +46,7 @@ export const SearchInput = ({ onChange, ...props }: SearchInputProps) => { {...props} rightSection={ ref.current?.value ? ( - <ActionIcon - icon="x" - onClick={handleClear} - variant="transparent" - /> + <ActionIcon icon="x" onClick={handleClear} variant="transparent" /> ) : null } /> diff --git a/src/renderer/features/sharing/components/share-item-context-modal.tsx b/src/renderer/features/sharing/components/share-item-context-modal.tsx index cc5683a5..532643a8 100644 --- a/src/renderer/features/sharing/components/share-item-context-modal.tsx +++ b/src/renderer/features/sharing/components/share-item-context-modal.tsx @@ -128,18 +128,10 @@ export const ShareItemContextModal = ({ <Group justify="flex-end"> <Group> - <Button - onClick={() => closeModal(id)} - size="md" - variant="subtle" - > + <Button onClick={() => closeModal(id)} size="md" variant="subtle"> {t('common.cancel', { postProcess: 'titleCase' })} </Button> - <Button - size="md" - type="submit" - variant="filled" - > + <Button size="md" type="submit" variant="filled"> {t('common.share', { postProcess: 'titleCase' })} </Button> </Group> diff --git a/src/renderer/features/sidebar/components/action-bar.tsx b/src/renderer/features/sidebar/components/action-bar.tsx index d772ed06..8732a85a 100644 --- a/src/renderer/features/sidebar/components/action-bar.tsx +++ b/src/renderer/features/sidebar/components/action-bar.tsx @@ -20,16 +20,8 @@ export const ActionBar = () => { const { open } = useCommandPalette(); return ( - <div - className={styles.container} - ref={cq.ref} - > - <Grid - display="flex" - gutter="sm" - px="1rem" - w="100%" - > + <div className={styles.container} ref={cq.ref}> + <Grid display="flex" gutter="sm" px="1rem" w="100%"> <Grid.Col span={6}> <TextInput leftSection={<Icon icon="search" />} @@ -44,11 +36,7 @@ export const ActionBar = () => { /> </Grid.Col> <Grid.Col span={6}> - <Group - gap="sm" - grow - wrap="nowrap" - > + <Group gap="sm" grow wrap="nowrap"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button p="0.5rem"> @@ -59,16 +47,10 @@ export const ActionBar = () => { <AppMenu /> </DropdownMenu.Dropdown> </DropdownMenu> - <Button - onClick={() => navigate(-1)} - p="0.5rem" - > + <Button onClick={() => navigate(-1)} p="0.5rem"> <Icon icon="arrowLeftS" /> </Button> - <Button - onClick={() => navigate(1)} - p="0.5rem" - > + <Button onClick={() => navigate(1)} p="0.5rem"> <Icon icon="arrowRightS" /> </Button> </Group> diff --git a/src/renderer/features/sidebar/components/collapsed-sidebar-button.tsx b/src/renderer/features/sidebar/components/collapsed-sidebar-button.tsx index 4fbdce41..3d3b1e33 100644 --- a/src/renderer/features/sidebar/components/collapsed-sidebar-button.tsx +++ b/src/renderer/features/sidebar/components/collapsed-sidebar-button.tsx @@ -9,12 +9,7 @@ interface CollapsedSidebarButtonProps extends ActionIconProps {} export const CollapsedSidebarButton = forwardRef<HTMLButtonElement, CollapsedSidebarButtonProps>( ({ children, ...props }: CollapsedSidebarButtonProps, ref) => { return ( - <ActionIcon - className={styles.button} - ref={ref} - variant="subtle" - {...props} - > + <ActionIcon className={styles.button} ref={ref} variant="subtle" {...props}> {children} </ActionIcon> ); diff --git a/src/renderer/features/sidebar/components/collapsed-sidebar.tsx b/src/renderer/features/sidebar/components/collapsed-sidebar.tsx index c6139249..6193daba 100644 --- a/src/renderer/features/sidebar/components/collapsed-sidebar.tsx +++ b/src/renderer/features/sidebar/components/collapsed-sidebar.tsx @@ -69,21 +69,12 @@ export const CollapsedSidebar = () => { > <ScrollArea> {sidebarCollapsedNavigation && ( - <Group - gap={0} - grow - > + <Group gap={0} grow> <CollapsedSidebarButton onClick={() => navigate(-1)}> - <Icon - icon="arrowLeftS" - size="xl" - /> + <Icon icon="arrowLeftS" size="xl" /> </CollapsedSidebarButton> <CollapsedSidebarButton onClick={() => navigate(1)}> - <Icon - icon="arrowRightS" - size="xl" - /> + <Icon icon="arrowRightS" size="xl" /> </CollapsedSidebarButton> </Group> )} @@ -92,13 +83,7 @@ export const CollapsedSidebar = () => { <CollapsedSidebarItem activeIcon={null} component={Flex} - icon={ - <Icon - fill="muted" - icon="menu" - size="3xl" - /> - } + icon={<Icon fill="muted" icon="menu" size="3xl" />} label={t('common.menu', { postProcess: 'titleCase' })} style={{ cursor: 'pointer', @@ -112,20 +97,9 @@ export const CollapsedSidebar = () => { </DropdownMenu> {sidebarItemsWithRoute.map((item) => ( <CollapsedSidebarItem - activeIcon={ - <SidebarIcon - active - route={item.route} - size="25" - /> - } + activeIcon={<SidebarIcon active route={item.route} size="25" />} component={NavLink} - icon={ - <SidebarIcon - route={item.route} - size="25" - /> - } + icon={<SidebarIcon route={item.route} size="25" />} key={item.id} label={item.label} route={item.route} diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index 7fa78d80..89423078 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -53,12 +53,7 @@ const PlaylistRowButton = ({ name, onPlay, to, ...props }: PlaylistRowButtonProp > {name} </SidebarItem> - {isHovered && ( - <RowControls - id={to} - onPlay={onPlay} - /> - )} + {isHovered && <RowControls id={to} onPlay={onPlay} />} </div> ); }; @@ -73,11 +68,7 @@ const RowControls = ({ const { t } = useTranslation(); return ( - <Group - className={styles.controls} - gap="xs" - wrap="nowrap" - > + <Group className={styles.controls} gap="xs" wrap="nowrap"> <ActionIcon icon="mediaPlay" iconProps={{ @@ -205,15 +196,8 @@ export const SidebarPlaylistList = () => { return ( <Accordion.Item value="playlists"> - <Accordion.Control - component="div" - role="button" - style={{ userSelect: 'none' }} - > - <Group - justify="space-between" - pr="var(--theme-spacing-md)" - > + <Accordion.Control component="div" role="button" style={{ userSelect: 'none' }}> + <Group justify="space-between" pr="var(--theme-spacing-md)"> <Text fw={600}> {t('page.sidebar.playlists', { postProcess: 'titleCase', @@ -323,10 +307,7 @@ export const SidebarSharedPlaylistList = () => { return ( <Accordion.Item value="shared-playlists"> <Accordion.Control> - <Text - fw={600} - variant="secondary" - > + <Text fw={600} variant="secondary"> {t('page.sidebar.shared', { postProcess: 'titleCase', })} diff --git a/src/renderer/features/sidebar/components/sidebar.tsx b/src/renderer/features/sidebar/components/sidebar.tsx index 8bc361f1..3a0dea0e 100644 --- a/src/renderer/features/sidebar/components/sidebar.tsx +++ b/src/renderer/features/sidebar/components/sidebar.tsx @@ -109,10 +109,7 @@ export const Sidebar = () => { })} id="left-sidebar" > - <Group - grow - id="global-search-container" - > + <Group grow id="global-search-container"> <ActionBar /> </Group> <ScrollArea @@ -134,10 +131,7 @@ export const Sidebar = () => { > <Accordion.Item value="library"> <Accordion.Control> - <Text - fw={600} - variant="secondary" - > + <Text fw={600} variant="secondary"> {t('page.sidebar.myLibrary', { postProcess: 'titleCase', })} @@ -146,10 +140,7 @@ export const Sidebar = () => { <Accordion.Panel> {sidebarItemsWithRoute.map((item) => { return ( - <SidebarItem - key={`sidebar-${item.route}`} - to={item.route} - > + <SidebarItem key={`sidebar-${item.route}`} to={item.route}> <Group gap="sm"> <SidebarIcon active={location.pathname === item.route} @@ -170,10 +161,7 @@ export const Sidebar = () => { )} </Accordion> </ScrollArea> - <AnimatePresence - initial={false} - mode="popLayout" - > + <AnimatePresence initial={false} mode="popLayout"> {showImage && ( <motion.div animate={{ opacity: 1, y: 0 }} diff --git a/src/renderer/features/similar-songs/components/similar-songs-list.tsx b/src/renderer/features/similar-songs/components/similar-songs-list.tsx index fd8663bd..cd36a76d 100644 --- a/src/renderer/features/similar-songs/components/similar-songs-list.tsx +++ b/src/renderer/features/similar-songs/components/similar-songs-list.tsx @@ -53,10 +53,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr }; return songQuery.isLoading ? ( - <Spinner - container - size={25} - /> + <Spinner container size={25} /> ) : ( <ErrorBoundary FallbackComponent={ErrorFallback}> <VirtualGridAutoSizerContainer> diff --git a/src/renderer/features/songs/components/jellyfin-song-filters.tsx b/src/renderer/features/songs/components/jellyfin-song-filters.tsx index a516f13a..e1130a67 100644 --- a/src/renderer/features/songs/components/jellyfin-song-filters.tsx +++ b/src/renderer/features/songs/components/jellyfin-song-filters.tsx @@ -175,16 +175,9 @@ export const JellyfinSongFilters = ({ return ( <Stack p="0.8rem"> {yesNoFilters.map((filter) => ( - <Group - justify="space-between" - key={`nd-filter-${filter.label}`} - > + <Group justify="space-between" key={`nd-filter-${filter.label}`}> <Text>{filter.label}</Text> - <YesNoSelect - onChange={filter.onChange} - size="xs" - value={filter.value} - /> + <YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} /> </Group> ))} <Divider my="0.5rem" /> diff --git a/src/renderer/features/songs/components/navidrome-song-filters.tsx b/src/renderer/features/songs/components/navidrome-song-filters.tsx index ad882f72..824f1a41 100644 --- a/src/renderer/features/songs/components/navidrome-song-filters.tsx +++ b/src/renderer/features/songs/components/navidrome-song-filters.tsx @@ -133,16 +133,9 @@ export const NavidromeSongFilters = ({ return ( <Stack p="0.8rem"> {toggleFilters.map((filter) => ( - <Group - justify="space-between" - key={`nd-filter-${filter.label}`} - > + <Group justify="space-between" key={`nd-filter-${filter.label}`}> <Text>{filter.label}</Text> - <YesNoSelect - onChange={filter.onChange} - size="xs" - value={filter.value} - /> + <YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} /> </Group> ))} <Divider my="0.5rem" /> @@ -170,10 +163,7 @@ export const NavidromeSongFilters = ({ {tagsQuery.data?.enumTags?.length && tagsQuery.data.enumTags.length > 0 && tagsQuery.data.enumTags.map((tag) => ( - <Group - grow - key={tag.name} - > + <Group grow key={tag.name}> <SelectWithInvalidData clearable data={tag.options} diff --git a/src/renderer/features/songs/components/song-list-content.tsx b/src/renderer/features/songs/components/song-list-content.tsx index db41b1a8..3cf18383 100644 --- a/src/renderer/features/songs/components/song-list-content.tsx +++ b/src/renderer/features/songs/components/song-list-content.tsx @@ -35,15 +35,9 @@ export const SongListContent = ({ gridRef, itemCount, tableRef }: SongListConten return ( <Suspense fallback={<Spinner container />}> {isGrid ? ( - <SongListGridView - gridRef={gridRef} - itemCount={itemCount} - /> + <SongListGridView gridRef={gridRef} itemCount={itemCount} /> ) : ( - <SongListTableView - itemCount={itemCount} - tableRef={tableRef} - /> + <SongListTableView itemCount={itemCount} tableRef={tableRef} /> )} </Suspense> ); diff --git a/src/renderer/features/songs/components/song-list-header-filters.tsx b/src/renderer/features/songs/components/song-list-header-filters.tsx index 4afe9281..d019fa01 100644 --- a/src/renderer/features/songs/components/song-list-header-filters.tsx +++ b/src/renderer/features/songs/components/song-list-header-filters.tsx @@ -484,11 +484,7 @@ export const SongListHeaderFilters = ({ return ( <Flex justify="space-between"> - <Group - gap="sm" - ref={cq.ref} - w="100%" - > + <Group gap="sm" ref={cq.ref} w="100%"> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> <Button variant="subtle">{sortByLabel}</Button> @@ -534,10 +530,7 @@ export const SongListHeaderFilters = ({ </DropdownMenu> </> )} - <FilterButton - isActive={!!isFilterApplied} - onClick={handleOpenFiltersModal} - /> + <FilterButton isActive={!!isFilterApplied} onClick={handleOpenFiltersModal} /> <RefreshButton onClick={handleRefresh} /> <DropdownMenu position="bottom-start"> <DropdownMenu.Target> @@ -578,10 +571,7 @@ export const SongListHeaderFilters = ({ </DropdownMenu.Dropdown> </DropdownMenu> </Group> - <Group - gap="sm" - wrap="nowrap" - > + <Group gap="sm" wrap="nowrap"> <ListConfigMenu autoFitColumns={table.autoFit} displayType={display} diff --git a/src/renderer/features/songs/components/song-list-header.tsx b/src/renderer/features/songs/components/song-list-header.tsx index f2abc778..c05c343b 100644 --- a/src/renderer/features/songs/components/song-list-header.tsx +++ b/src/renderer/features/songs/components/song-list-header.tsx @@ -70,15 +70,9 @@ export const SongListHeader = ({ const playButtonBehavior = usePlayButtonBehavior(); return ( - <Stack - gap={0} - ref={cq.ref} - > + <Stack gap={0} ref={cq.ref}> <PageHeader> - <Flex - justify="space-between" - w="100%" - > + <Flex justify="space-between" w="100%"> <LibraryHeaderBar> <LibraryHeaderBar.PlayButton onClick={() => handlePlay?.({ playType: playButtonBehavior })} @@ -93,10 +87,7 @@ export const SongListHeader = ({ </LibraryHeaderBar.Badge> </LibraryHeaderBar> <Group> - <SearchInput - defaultValue={filter.searchTerm} - onChange={handleSearch} - /> + <SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} /> </Group> </Flex> </PageHeader> diff --git a/src/renderer/features/songs/components/subsonic-song-filter.tsx b/src/renderer/features/songs/components/subsonic-song-filter.tsx index 1b508d4c..3c15aec3 100644 --- a/src/renderer/features/songs/components/subsonic-song-filter.tsx +++ b/src/renderer/features/songs/components/subsonic-song-filter.tsx @@ -84,10 +84,7 @@ export const SubsonicSongFilters = ({ return ( <Stack p="0.8rem"> {toggleFilters.map((filter) => ( - <Group - justify="space-between" - key={`ss-filter-${filter.label}`} - > + <Group justify="space-between" key={`ss-filter-${filter.label}`}> <Text>{filter.label}</Text> <Switch checked={filter?.value || false} diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index d24a4cf1..c64d0153 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -132,11 +132,7 @@ const TrackListRoute = () => { tableRef={tableRef} title={title} /> - <SongListContent - gridRef={gridRef} - itemCount={itemCount} - tableRef={tableRef} - /> + <SongListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> </ListContext.Provider> </AnimatedPage> ); diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index ec2b41fc..9c56a35e 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -10,6 +10,7 @@ import { ServerList } from '/@/renderer/features/servers'; import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form'; import { AppRoute } from '/@/renderer/router/routes'; import { + useAppStore, useAppStoreActions, useAuthStoreActions, useCurrentServer, @@ -18,6 +19,7 @@ import { } from '/@/renderer/store'; import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { Icon } from '/@/shared/components/icon/icon'; +import { toast } from '/@/shared/components/toast/toast'; import { ServerListItem, ServerType } from '/@/shared/types/domain-types'; const browser = isElectron() ? window.api.browser : null; @@ -30,7 +32,8 @@ export const AppMenu = () => { const serverList = useServerList(); const { setCurrentServer } = useAuthStoreActions(); const { collapsed } = useSidebarStore(); - const { setSideBar } = useAppStoreActions(); + const { privateMode } = useAppStore(); + const { setPrivateMode, setSideBar } = useAppStoreActions(); const handleSetCurrentServer = (server: ServerListItem) => { navigate(AppRoute.HOME); @@ -80,6 +83,22 @@ export const AppMenu = () => { setSideBar({ collapsed: false }); }; + const handlePrivateModeOff = () => { + setPrivateMode(false); + toast.info({ + message: t('form.privateMode.disabled', { postProcess: 'sentenceCase' }), + title: t('form.privateMode.title', { postProcess: 'sentenceCase' }), + }); + }; + + const handlePrivateModeOn = () => { + setPrivateMode(true); + toast.info({ + message: t('form.privateMode.enabled', { postProcess: 'sentenceCase' }), + title: t('form.privateMode.title', { postProcess: 'sentenceCase' }), + }); + }; + const handleQuit = () => { browser?.quit(); }; @@ -127,7 +146,21 @@ export const AppMenu = () => { > {t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })} </DropdownMenu.Item> - + {privateMode ? ( + <DropdownMenu.Item + leftSection={<Icon color="error" icon="lock" />} + onClick={handlePrivateModeOff} + > + {t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' })} + </DropdownMenu.Item> + ) : ( + <DropdownMenu.Item + leftSection={<Icon icon="lockOpen" />} + onClick={handlePrivateModeOn} + > + {t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' })} + </DropdownMenu.Item> + )} <DropdownMenu.Divider /> <DropdownMenu.Label> {t('page.appMenu.selectServer', { postProcess: 'sentenceCase' })} @@ -144,10 +177,7 @@ export const AppMenu = () => { key={`server-${server.id}`} leftSection={ isSessionExpired ? ( - <Icon - fill="error" - icon="lock" - /> + <Icon fill="error" icon="lock" /> ) : ( <Icon color={server.id === currentServer?.id ? 'primary' : undefined} @@ -186,10 +216,7 @@ export const AppMenu = () => { > {t('page.appMenu.openBrowserDevtools', { postProcess: 'sentenceCase' })} </DropdownMenu.Item> - <DropdownMenu.Item - leftSection={<Icon icon="x" />} - onClick={handleQuit} - > + <DropdownMenu.Item leftSection={<Icon icon="x" />} onClick={handleQuit}> {t('page.appMenu.quit', { postProcess: 'sentenceCase' })} </DropdownMenu.Item> </> diff --git a/src/renderer/hooks/use-display-refresh.ts b/src/renderer/hooks/use-display-refresh.ts index 25aefc20..21304cfb 100644 --- a/src/renderer/hooks/use-display-refresh.ts +++ b/src/renderer/hooks/use-display-refresh.ts @@ -60,13 +60,14 @@ export const useDisplayRefresh = <TFilter>({ (e: ChangeEvent<HTMLInputElement>) => { const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = setFilter({ + customFilters, data: { searchTerm }, itemType, key: pageKey, }); return updatedFilters; }, - [itemType, pageKey, setFilter], + [customFilters, itemType, pageKey, setFilter], ); return { customFilters, filter, handlePlay, refresh, search }; diff --git a/src/renderer/is-updated-dialog.tsx b/src/renderer/is-updated-dialog.tsx index 85f4136b..02b10266 100644 --- a/src/renderer/is-updated-dialog.tsx +++ b/src/renderer/is-updated-dialog.tsx @@ -34,10 +34,7 @@ export function IsUpdatedDialog() { > <Stack> <Text>{t('common.newVersion', { postProcess: 'sentenceCase', version })}</Text> - <Group - justify="flex-end" - wrap="nowrap" - > + <Group justify="flex-end" wrap="nowrap"> <Button component="a" href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`} @@ -48,10 +45,7 @@ export function IsUpdatedDialog() { > {t('common.viewReleaseNotes', { postProcess: 'sentenceCase' })} </Button> - <Button - onClick={handleDismiss} - variant="default" - > + <Button onClick={handleDismiss} variant="default"> {t('common.dismiss', { postProcess: 'titleCase' })} </Button> </Group> diff --git a/src/renderer/layouts/default-layout/left-sidebar.tsx b/src/renderer/layouts/default-layout/left-sidebar.tsx index 49d53049..6a406a2b 100644 --- a/src/renderer/layouts/default-layout/left-sidebar.tsx +++ b/src/renderer/layouts/default-layout/left-sidebar.tsx @@ -17,10 +17,7 @@ export const LeftSidebar = ({ isResizing, startResizing }: LeftSidebarProps) => const { collapsed } = useSidebarStore(); return ( - <aside - className={styles.container} - id="sidebar" - > + <aside className={styles.container} id="sidebar"> <ResizeHandle isResizing={isResizing} onMouseDown={(e) => { diff --git a/src/renderer/layouts/default-layout/main-content.tsx b/src/renderer/layouts/default-layout/main-content.tsx index d1d0abd9..b07fb1a4 100644 --- a/src/renderer/layouts/default-layout/main-content.tsx +++ b/src/renderer/layouts/default-layout/main-content.tsx @@ -109,10 +109,7 @@ export const MainContent = ({ shell }: { shell?: boolean }) => { {showQueueDrawerButton && <SideDrawerQueue />} </Suspense> <FullScreenOverlay /> - <LeftSidebar - isResizing={isResizing} - startResizing={startResizing} - /> + <LeftSidebar isResizing={isResizing} startResizing={startResizing} /> <RightSidebar isResizing={isResizingRight} ref={rightSidebarRef} diff --git a/src/renderer/layouts/default-layout/right-sidebar.tsx b/src/renderer/layouts/default-layout/right-sidebar.tsx index 5674b577..43d31a85 100644 --- a/src/renderer/layouts/default-layout/right-sidebar.tsx +++ b/src/renderer/layouts/default-layout/right-sidebar.tsx @@ -83,12 +83,7 @@ export const RightSidebar = forwardRef( const showSideQueue = rightExpanded && location.pathname !== AppRoute.NOW_PLAYING; return ( - <AnimatePresence - initial={false} - key="queue-sidebar" - mode="sync" - presenceAffectsLayout - > + <AnimatePresence initial={false} key="queue-sidebar" mode="sync" presenceAffectsLayout> {showSideQueue && ( <> {sideQueueType === 'sideQueue' ? ( diff --git a/src/renderer/layouts/default-layout/side-drawer-queue.tsx b/src/renderer/layouts/default-layout/side-drawer-queue.tsx index a0e27d28..ef791a10 100644 --- a/src/renderer/layouts/default-layout/side-drawer-queue.tsx +++ b/src/renderer/layouts/default-layout/side-drawer-queue.tsx @@ -81,10 +81,7 @@ export const SideDrawerQueue = () => { !rightExpanded && !drawer && location.pathname !== AppRoute.NOW_PLAYING; return ( - <AnimatePresence - initial={false} - mode="wait" - > + <AnimatePresence initial={false} mode="wait"> {isQueueDrawerButtonVisible && ( <motion.div animate="visible" @@ -97,10 +94,7 @@ export const SideDrawerQueue = () => { variants={queueDrawerButtonVariants} whileHover={{ opacity: 1, scale: 2, transition: { duration: 0.5 } }} > - <Icon - icon="arrowLeftToLine" - size="lg" - /> + <Icon icon="arrowLeftToLine" size="lg" /> </motion.div> )} diff --git a/src/renderer/layouts/window-bar.tsx b/src/renderer/layouts/window-bar.tsx index 4fa5ac74..3d65bd1d 100644 --- a/src/renderer/layouts/window-bar.tsx +++ b/src/renderer/layouts/window-bar.tsx @@ -12,8 +12,12 @@ import macMinHover from './assets/min-mac-hover.png'; import macMin from './assets/min-mac.png'; import styles from './window-bar.module.css'; -import { useCurrentStatus, useQueueStatus } from '/@/renderer/store'; -import { useWindowSettings } from '/@/renderer/store/settings.store'; +import { + useAppStore, + useCurrentStatus, + useQueueStatus, + useWindowSettings, +} from '/@/renderer/store'; import { Text } from '/@/shared/components/text/text'; import { Platform, PlayerStatus } from '/@/shared/types/types'; @@ -40,27 +44,14 @@ const WindowsControls = ({ controls, title }: WindowBarControlsProps) => { return ( <div className={styles.windowsContainer}> <div className={styles.playerStatusContainer}> - <img - alt="" - height={18} - src={appIcon} - width={18} - /> + <img alt="" height={18} src={appIcon} width={18} /> <Text>{title}</Text> </div> <div className={styles.windowsButtonGroup}> - <div - className={styles.windowsButton} - onClick={handleMinimize} - role="button" - > + <div className={styles.windowsButton} onClick={handleMinimize} role="button"> <RiSubtractLine size={19} /> </div> - <div - className={styles.windowsButton} - onClick={handleMaximize} - role="button" - > + <div className={styles.windowsButton} onClick={handleMaximize} role="button"> <RiCheckboxBlankLine size={13} /> </div> <div @@ -139,14 +130,16 @@ export const WindowBar = () => { const playerStatus = useCurrentStatus(); const { currentSong, index, length } = useQueueStatus(); const { windowBarStyle } = useWindowSettings(); + const { privateMode } = useAppStore(); const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : ''; const queueString = length ? `(${index + 1} / ${length}) ` : ''; - const title = length - ? currentSong?.artistName - ? `${statusString}${queueString}${currentSong?.name} — ${currentSong?.artistName}` - : `${statusString}${queueString}${currentSong?.name}` - : 'Feishin'; + const privateModeString = privateMode ? '(Private mode)' : ''; + const title = `${ + length + ? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName ? ` — ${currentSong?.artistName}` : ''}` + : 'Feishin' + }${privateMode ? ` ${privateModeString}` : ''}`; document.title = title; const [max, setMax] = useState(localSettings?.env.START_MAXIMIZED || false); diff --git a/src/renderer/router/app-outlet.tsx b/src/renderer/router/app-outlet.tsx index be1e31a9..6418db92 100644 --- a/src/renderer/router/app-outlet.tsx +++ b/src/renderer/router/app-outlet.tsx @@ -45,22 +45,14 @@ export const AppOutlet = () => { if (authState === AuthState.LOADING) { return ( - <Center - h="100vh" - w="100%" - > + <Center h="100vh" w="100%"> <Spinner container /> </Center> ); } if (isActionsRequired || authState === AuthState.INVALID) { - return ( - <Navigate - replace - to={AppRoute.ACTION_REQUIRED} - /> - ); + return <Navigate replace to={AppRoute.ACTION_REQUIRED} />; } return <Outlet />; diff --git a/src/renderer/router/app-router.tsx b/src/renderer/router/app-router.tsx index bfd70abe..94d559e6 100644 --- a/src/renderer/router/app-router.tsx +++ b/src/renderer/router/app-router.tsx @@ -79,10 +79,7 @@ export const AppRouter = () => { > <Routes> <Route element={<TitlebarOutlet />}> - <Route - element={<AppOutlet />} - errorElement={<RouteErrorBoundary />} - > + <Route element={<AppOutlet />} errorElement={<RouteErrorBoundary />}> <Route element={<DefaultLayout />}> <Route element={<HomeRoute />} @@ -140,10 +137,7 @@ export const AppRouter = () => { path={AppRoute.LIBRARY_ARTISTS} /> <Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}> - <Route - element={<AlbumArtistDetailRoute />} - index - /> + <Route element={<AlbumArtistDetailRoute />} index /> <Route element={<AlbumListRoute />} path={AppRoute.LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY} @@ -181,15 +175,9 @@ export const AppRouter = () => { errorElement={<RouteErrorBoundary />} path={AppRoute.LIBRARY_ALBUM_ARTISTS} > - <Route - element={<AlbumArtistListRoute />} - index - /> + <Route element={<AlbumArtistListRoute />} index /> <Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}> - <Route - element={<AlbumArtistDetailRoute />} - index - /> + <Route element={<AlbumArtistDetailRoute />} index /> <Route element={<AlbumListRoute />} path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY} @@ -204,10 +192,7 @@ export const AppRouter = () => { /> </Route> </Route> - <Route - element={<InvalidRoute />} - path="*" - /> + <Route element={<InvalidRoute />} path="*" /> </Route> </Route> </Route> diff --git a/src/renderer/store/app.store.ts b/src/renderer/store/app.store.ts index 83074f5d..68f5bfd3 100644 --- a/src/renderer/store/app.store.ts +++ b/src/renderer/store/app.store.ts @@ -8,6 +8,7 @@ import { Platform } from '/@/shared/types/types'; export interface AppSlice extends AppState { actions: { setAppStore: (data: Partial<AppSlice>) => void; + setPrivateMode: (enabled: boolean) => void; setSideBar: (options: Partial<SidebarProps>) => void; setTitleBar: (options: Partial<TitlebarProps>) => void; }; @@ -17,6 +18,7 @@ export interface AppState { commandPalette: CommandPaletteProps; isReorderingQueue: boolean; platform: Platform; + privateMode: boolean; sidebar: SidebarProps; titlebar: TitlebarProps; } @@ -50,6 +52,11 @@ export const useAppStore = createWithEqualityFn<AppSlice>()( setAppStore: (data) => { set({ ...get(), ...data }); }, + setPrivateMode: (privateMode) => { + set((state) => { + state.privateMode = privateMode; + }); + }, setSideBar: (options) => { set((state) => { state.sidebar = { ...state.sidebar, ...options }; @@ -81,6 +88,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()( }, isReorderingQueue: false, platform: Platform.WINDOWS, + privateMode: false, sidebar: { collapsed: false, expanded: [], diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index cec445ef..7514e6c1 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -157,6 +157,12 @@ export enum BindingActions { ZOOM_OUT = 'zoomOut', } +export enum DiscordDisplayType { + ARTIST_NAME = 'artist', + FEISHIN = 'feishin', + SONG_NAME = 'song', +} + export enum GenreTarget { ALBUM = 'album', TRACK = 'track', @@ -198,6 +204,7 @@ export interface SettingsState { }; discord: { clientId: string; + displayType: DiscordDisplayType; enabled: boolean; showAsListening: boolean; showPaused: boolean; @@ -353,6 +360,7 @@ const initialState: SettingsState = { }, discord: { clientId: '1165957668758900787', + displayType: DiscordDisplayType.FEISHIN, enabled: false, showAsListening: false, showPaused: true, diff --git a/src/renderer/utils/format.tsx b/src/renderer/utils/format.tsx index 940e4d96..335556d1 100644 --- a/src/renderer/utils/format.tsx +++ b/src/renderer/utils/format.tsx @@ -53,12 +53,7 @@ export const formatDurationString = (duration: number) => { }; export const formatRating = (item: Album | AlbumArtist | Song) => - item.userRating !== null ? ( - <Rating - readOnly - value={item.userRating} - /> - ) : null; + item.userRating !== null ? <Rating readOnly value={item.userRating} /> : null; const SIZES = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; diff --git a/src/renderer/utils/linkify.tsx b/src/renderer/utils/linkify.tsx index 2469f48e..aed85628 100644 --- a/src/renderer/utils/linkify.tsx +++ b/src/renderer/utils/linkify.tsx @@ -20,12 +20,7 @@ export const replaceURLWithHTMLLinks = (text: string) => { const link = match[0]; const prefix = link.startsWith('http') ? '' : 'https://'; elements.push( - <a - href={prefix + link} - key={lastIndex} - rel="noopener noreferrer" - target="_blank" - > + <a href={prefix + link} key={lastIndex} rel="noopener noreferrer" target="_blank"> {link} </a>, ); diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index 0d453ff2..cb522f63 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -170,6 +170,47 @@ const normalizeSong = ( deviceId: string, imageSize?: number, ): Song => { + let bitRate = 0; + let channels: null | number = null; + let container: null | string = null; + let path: null | string = null; + let sampleRate: null | number = null; + let size = 0; + let streamUrl = ''; + + if (item.MediaSources?.length) { + const source = item.MediaSources[0]; + + container = source.Container; + path = source.Path; + size = source.Size; + + streamUrl = getStreamUrl({ + container: container, + deviceId, + eTag: source.ETag, + id: item.Id, + mediaSourceId: source.Id, + server, + }); + + if ((source.MediaStreams?.length || 0) > 0) { + for (const stream of source.MediaStreams) { + if (stream.Type === 'Audio') { + bitRate = + stream.BitRate !== undefined + ? Number(Math.trunc(stream.BitRate / 1000)) + : 0; + channels = stream.Channels || null; + sampleRate = stream.SampleRate || null; + break; + } + } + } + } else { + console.warn('Jellyfin song retrieved with no media sources', item); + } + return { album: item.Album, albumArtists: item.AlbumArtists?.map((entry) => ({ @@ -184,14 +225,13 @@ const normalizeSong = ( imageUrl: null, name: entry.Name, })), - bitRate: - item.MediaSources?.[0].Bitrate && - Number(Math.trunc(item.MediaSources[0].Bitrate / 1000)), + bitDepth: null, + bitRate, bpm: null, - channels: null, + channels, comment: null, compilation: null, - container: (item.MediaSources && item.MediaSources[0]?.Container) || null, + container, createdAt: item.DateCreated, discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1, discSubtitle: null, @@ -220,7 +260,7 @@ const normalizeSong = ( lyrics: null, name: item.Name, participants: getPeople(item), - path: (item.MediaSources && item.MediaSources[0]?.Path) || null, + path, peak: null, playCount: (item.UserData && item.UserData.PlayCount) || 0, playlistItemId: item.PlaylistItemId, @@ -230,17 +270,11 @@ const normalizeSong = ( ? new Date(item.ProductionYear, 0, 1).toISOString() : null, releaseYear: item.ProductionYear ? String(item.ProductionYear) : null, + sampleRate, serverId: server?.id || '', serverType: ServerType.JELLYFIN, - size: item.MediaSources && item.MediaSources[0]?.Size, - streamUrl: getStreamUrl({ - container: item.MediaSources?.[0]?.Container, - deviceId, - eTag: item.MediaSources?.[0]?.ETag, - id: item.Id, - mediaSourceId: item.MediaSources?.[0]?.Id, - server, - }), + size, + streamUrl, tags: getTags(item), trackNumber: item.IndexNumber, uniqueId: nanoid(), diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index 4c624b54..af5f9e61 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -148,6 +148,7 @@ const normalizeSong = ( albumId: item.albumId, ...getArtists(item), artistName: item.artist, + bitDepth: item.bitDepth || null, bitRate: item.bitRate, bpm: item.bpm ? item.bpm : null, channels: item.channels ? item.channels : null, @@ -189,6 +190,7 @@ const normalizeSong = ( : new Date(Date.UTC(item.year, 0, 1)) ).toISOString(), releaseYear: String(item.year), + sampleRate: item.sampleRate || null, serverId: server?.id || 'unknown', serverType: ServerType.NAVIDROME, size: item.size, diff --git a/src/shared/api/navidrome/navidrome-types.ts b/src/shared/api/navidrome/navidrome-types.ts index 631af88e..39a0c002 100644 --- a/src/shared/api/navidrome/navidrome-types.ts +++ b/src/shared/api/navidrome/navidrome-types.ts @@ -184,6 +184,7 @@ const song = z.object({ albumId: z.string(), artist: z.string(), artistId: z.string(), + bitDepth: z.number().optional(), bitRate: z.number(), bookmarkPosition: z.number(), bpm: z.number().optional(), @@ -226,6 +227,7 @@ const song = z.object({ rgAlbumPeak: z.number().optional(), rgTrackGain: z.number().optional(), rgTrackPeak: z.number().optional(), + sampleRate: z.number(), size: z.number(), smallImageUrl: z.string().optional(), sortAlbumArtistName: z.string(), diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index a380868e..83098c0a 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -135,9 +135,10 @@ const normalizeSong = ( albumId: item.albumId?.toString() || '', artistName: item.artist || '', artists: getArtistList(item.artists, item.artistId, item.artist), + bitDepth: item.bitDepth || null, bitRate: item.bitRate || 0, bpm: item.bpm || null, - channels: null, + channels: item.channelCount || null, comment: null, compilation: null, container: item.contentType, @@ -172,6 +173,7 @@ const normalizeSong = ( playCount: item?.playCount || 0, releaseDate: null, releaseYear: item.year ? String(item.year) : null, + sampleRate: item.samplingRate || null, serverId: server?.id || 'unknown', serverType: ServerType.SUBSONIC, size: item.size, diff --git a/src/shared/api/subsonic/subsonic-types.ts b/src/shared/api/subsonic/subsonic-types.ts index 3ab30ae4..82a182e0 100644 --- a/src/shared/api/subsonic/subsonic-types.ts +++ b/src/shared/api/subsonic/subsonic-types.ts @@ -85,8 +85,10 @@ const song = z.object({ artistId: id.optional(), artists: z.array(simpleArtist), averageRating: z.number().optional(), + bitDepth: z.number().optional(), bitRate: z.number().optional(), bpm: z.number().optional(), + channelCount: z.number().optional(), contentType: z.string(), contributors: z.array(contributor).optional(), coverArt: z.string().optional(), @@ -103,6 +105,7 @@ const song = z.object({ path: z.string(), playCount: z.number().optional(), replayGain: songGain.optional(), + samplingRate: z.number().optional(), size: z.number(), starred: z.boolean().optional(), suffix: z.string(), diff --git a/src/shared/components/accordion/accordion.tsx b/src/shared/components/accordion/accordion.tsx index 2fb6379a..0c6fe90e 100644 --- a/src/shared/components/accordion/accordion.tsx +++ b/src/shared/components/accordion/accordion.tsx @@ -17,12 +17,7 @@ export interface AccordionProps export const Accordion = ({ children, classNames, ...props }: AccordionProps) => { return ( <MantineAccordion - chevron={ - <Icon - icon="arrowUpS" - size="lg" - /> - } + chevron={<Icon icon="arrowUpS" size="lg" />} classNames={{ chevron: styles.chevron, control: styles.control, diff --git a/src/shared/components/action-icon/action-icon.tsx b/src/shared/components/action-icon/action-icon.tsx index b25877d6..844c3a48 100644 --- a/src/shared/components/action-icon/action-icon.tsx +++ b/src/shared/components/action-icon/action-icon.tsx @@ -45,19 +45,9 @@ const _ActionIcon = forwardRef<HTMLButtonElement, ActionIconProps>( if (tooltip && icon) { return ( - <Tooltip - withinPortal - {...tooltip} - > - <MantineActionIcon - ref={ref} - {...actionIconProps} - > - <Icon - icon={icon} - size={actionIconProps.size} - {...iconProps} - /> + <Tooltip withinPortal {...tooltip}> + <MantineActionIcon ref={ref} {...actionIconProps}> + <Icon icon={icon} size={actionIconProps.size} {...iconProps} /> </MantineActionIcon> </Tooltip> ); @@ -65,29 +55,16 @@ const _ActionIcon = forwardRef<HTMLButtonElement, ActionIconProps>( if (icon) { return ( - <MantineActionIcon - ref={ref} - {...actionIconProps} - > - <Icon - icon={icon} - size={actionIconProps.size} - {...iconProps} - /> + <MantineActionIcon ref={ref} {...actionIconProps}> + <Icon icon={icon} size={actionIconProps.size} {...iconProps} /> </MantineActionIcon> ); } if (tooltip) { return ( - <Tooltip - withinPortal - {...tooltip} - > - <MantineActionIcon - ref={ref} - {...actionIconProps} - > + <Tooltip withinPortal {...tooltip}> + <MantineActionIcon ref={ref} {...actionIconProps}> {children} </MantineActionIcon> </Tooltip> @@ -95,10 +72,7 @@ const _ActionIcon = forwardRef<HTMLButtonElement, ActionIconProps>( } return ( - <MantineActionIcon - ref={ref} - {...actionIconProps} - > + <MantineActionIcon ref={ref} {...actionIconProps}> {children} </MantineActionIcon> ); diff --git a/src/shared/components/button/button.tsx b/src/shared/components/button/button.tsx index de864128..d729b70e 100644 --- a/src/shared/components/button/button.tsx +++ b/src/shared/components/button/button.tsx @@ -44,10 +44,7 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>( ) => { if (tooltip) { return ( - <Tooltip - withinPortal - {...tooltip} - > + <Tooltip withinPortal {...tooltip}> <MantineButton autoContrast classNames={{ @@ -145,10 +142,7 @@ export const TimeoutButton = ({ timeoutProps, ...props }: TimeoutButtonProps) => }, [clear, isRunning, start]); return ( - <Button - onClick={startTimeout} - {...props} - > + <Button onClick={startTimeout} {...props}> {isRunning ? 'Cancel' : props.children} </Button> ); diff --git a/src/shared/components/dropdown-menu/dropdown-menu.tsx b/src/shared/components/dropdown-menu/dropdown-menu.tsx index e0a89246..1d674bd9 100644 --- a/src/shared/components/dropdown-menu/dropdown-menu.tsx +++ b/src/shared/components/dropdown-menu/dropdown-menu.tsx @@ -44,10 +44,7 @@ export const DropdownMenu = ({ children, ...props }: MenuProps) => { const MenuLabel = ({ children, ...props }: MenuLabelProps) => { return ( - <MantineMenu.Label - className={styles['menu-label']} - {...props} - > + <MantineMenu.Label className={styles['menu-label']} {...props}> {children} </MantineMenu.Label> ); @@ -75,10 +72,7 @@ const pMenuItem = ({ children, isDanger, isSelected, ...props }: MenuItemProps) const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => { return ( - <MantineMenu.Dropdown - className={styles['menu-dropdown']} - {...props} - > + <MantineMenu.Dropdown className={styles['menu-dropdown']} {...props}> {children} </MantineMenu.Dropdown> ); @@ -87,12 +81,7 @@ const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => { const MenuItem = createPolymorphicComponent<'button', MenuItemProps>(pMenuItem); const MenuDivider = ({ ...props }: MenuDividerProps) => { - return ( - <MantineMenu.Divider - className={styles['menu-divider']} - {...props} - /> - ); + return <MantineMenu.Divider className={styles['menu-divider']} {...props} />; }; DropdownMenu.Label = MenuLabel; diff --git a/src/shared/components/grid/grid.tsx b/src/shared/components/grid/grid.tsx index d6c1bdd3..0e63b6f1 100644 --- a/src/shared/components/grid/grid.tsx +++ b/src/shared/components/grid/grid.tsx @@ -3,13 +3,7 @@ import { Grid as MantineGrid, GridProps as MantineGridProps } from '@mantine/cor export interface GridProps extends MantineGridProps {} export const Grid = ({ classNames, style, ...props }: GridProps) => { - return ( - <MantineGrid - classNames={{ ...classNames }} - style={{ ...style }} - {...props} - /> - ); + return <MantineGrid classNames={{ ...classNames }} style={{ ...style }} {...props} />; }; Grid.Col = MantineGrid.Col; diff --git a/src/shared/components/icon/icon.tsx b/src/shared/components/icon/icon.tsx index d77b5552..fc4e3174 100644 --- a/src/shared/components/icon/icon.tsx +++ b/src/shared/components/icon/icon.tsx @@ -59,6 +59,7 @@ import { LuListPlus, LuLoader, LuLock, + LuLockOpen, LuLogIn, LuLogOut, LuMenu, @@ -163,6 +164,7 @@ export const AppIcon = { listInfinite: LuInfinity, listPaginated: LuArrowRightToLine, lock: LuLock, + lockOpen: LuLockOpen, mediaNext: LuSkipForward, mediaPause: LuPause, mediaPlay: LuPlay, diff --git a/src/shared/components/image/image.tsx b/src/shared/components/image/image.tsx index 5a6725bd..41c8d334 100644 --- a/src/shared/components/image/image.tsx +++ b/src/shared/components/image/image.tsx @@ -89,10 +89,7 @@ export function Image({ function ImageContainer({ children, className, enableAnimation, ...props }: ImageContainerProps) { if (!enableAnimation) { return ( - <div - className={clsx(styles.imageContainer, className)} - {...props} - > + <div className={clsx(styles.imageContainer, className)} {...props}> {children} </div> ); @@ -112,10 +109,7 @@ function ImageContainer({ children, className, enableAnimation, ...props }: Imag function ImageLoader({ className }: ImageLoaderProps) { return ( <div className={clsx(styles.loader, className)}> - <Skeleton - className={clsx(styles.skeleton, className)} - enableAnimation={true} - /> + <Skeleton className={clsx(styles.skeleton, className)} enableAnimation={true} /> </div> ); } @@ -123,10 +117,7 @@ function ImageLoader({ className }: ImageLoaderProps) { function ImageUnloader({ className }: ImageUnloaderProps) { return ( <div className={clsx(styles.unloader, className)}> - <Icon - icon="emptyImage" - size="xl" - /> + <Icon icon="emptyImage" size="xl" /> </div> ); } diff --git a/src/shared/components/modal/modal.tsx b/src/shared/components/modal/modal.tsx index 60da272a..135ae049 100644 --- a/src/shared/components/modal/modal.tsx +++ b/src/shared/components/modal/modal.tsx @@ -93,19 +93,10 @@ export const ConfirmModal = ({ <Stack> <Flex>{children}</Flex> <Group justify="flex-end"> - <Button - data-focus - onClick={handleCancel} - variant="default" - > + <Button data-focus onClick={handleCancel} variant="default"> {labels?.cancel ? labels.cancel : 'Cancel'} </Button> - <Button - disabled={disabled} - loading={loading} - onClick={onConfirm} - variant="filled" - > + <Button disabled={disabled} loading={loading} onClick={onConfirm} variant="filled"> {labels?.confirm ? labels.confirm : 'Confirm'} </Button> </Group> diff --git a/src/shared/components/option/option.tsx b/src/shared/components/option/option.tsx index bbb364a8..4cbeb5ae 100644 --- a/src/shared/components/option/option.tsx +++ b/src/shared/components/option/option.tsx @@ -12,11 +12,7 @@ interface OptionProps extends GroupProps { export const Option = ({ children, ...props }: OptionProps) => { return ( - <Group - classNames={{ root: styles.root }} - grow - {...props} - > + <Group classNames={{ root: styles.root }} grow {...props}> {children} </Group> ); diff --git a/src/shared/components/spinner/spinner.tsx b/src/shared/components/spinner/spinner.tsx index 3bb36e5f..b866d48c 100644 --- a/src/shared/components/spinner/spinner.tsx +++ b/src/shared/components/spinner/spinner.tsx @@ -16,20 +16,10 @@ export const Spinner = ({ ...props }: SpinnerProps) => { if (props.container) { return ( <Center className={styles.container}> - <SpinnerIcon - className={styles.icon} - color={props.color} - size={props.size} - /> + <SpinnerIcon className={styles.icon} color={props.color} size={props.size} /> </Center> ); } - return ( - <SpinnerIcon - className={styles.icon} - color={props.color} - size={props.size} - /> - ); + return <SpinnerIcon className={styles.icon} color={props.color} size={props.size} />; }; diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 448413fa..137d72e7 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -320,6 +320,7 @@ export type Song = { albumId: string; artistName: string; artists: RelatedArtist[]; + bitDepth: null | number; bitRate: number; bpm: null | number; channels: null | number; @@ -346,6 +347,7 @@ export type Song = { playlistItemId?: string; releaseDate: null | string; releaseYear: null | string; + sampleRate: null | number; serverId: string; serverType: ServerType; size: number;