diff --git a/.github/ISSUE_TEMPLATE/1-Bug_report.md b/.github/ISSUE_TEMPLATE/1-Bug_report.md deleted file mode 100644 index 8f2b5724e..000000000 --- a/.github/ISSUE_TEMPLATE/1-Bug_report.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: Bug report -about: You're having technical issues. 🐞 -labels: 'bug' ---- - -## Expected Behavior - - - -## Current Behavior - - - - - -## Steps to Reproduce - - - - -1. - -2. - -3. - -4. - -## Possible Solution (Not obligatory) - - - -## Context - - - -## Your Environment - - - -- Application version (e.g. v0.1.0) : -- Operating System and version (e.g. Windows 10) : -- Server and version (e.g. Navidrome v0.48.0) : -- Node version (if developing locally) : diff --git a/.github/ISSUE_TEMPLATE/2-Question.md b/.github/ISSUE_TEMPLATE/2-Question.md deleted file mode 100644 index aec9e1fc0..000000000 --- a/.github/ISSUE_TEMPLATE/2-Question.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: Question -about: Ask a question.❓ -labels: 'question' ---- - - - - diff --git a/.github/ISSUE_TEMPLATE/3-Feature_request.md b/.github/ISSUE_TEMPLATE/3-Feature_request.md deleted file mode 100644 index 0e02f8c15..000000000 --- a/.github/ISSUE_TEMPLATE/3-Feature_request.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Feature request -about: Request a feature to be added to Feishin 🎉 -labels: 'enhancement' ---- - -## What do you want to be added? - -## Additional context - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..4a452bb46 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +name: Bug report +description: You're having technical issues. 🐞 +labels: ['bug'] +body: + - type: textarea + attributes: + label: Expected Behavior + description: What should have happened? + validations: + required: true + - type: textarea + attributes: + label: Current Behavior + description: What went wrong? Add screenshots to help explain your problem. (Open the browser dev tools in the menu or using CTRL + SHIFT + I) + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + placeholder: | + + + 1. + 2. + 3. + 4. + validations: + required: true + - type: textarea + attributes: + label: Possible Solution + description: Suggest a reason for the bug or how to fix it. + validations: + required: false + - type: textarea + attributes: + label: Context + description: How has this issue affected you? What are you trying to accomplish? + validations: + required: false + - type: input + attributes: + label: Application version + placeholder: (e.g. v0.1.0) + validations: + required: true + - type: input + attributes: + label: Operating System and version + placeholder: (e.g. Windows 11 desktop, Webapp in Firefox) + validations: + required: true + - type: input + attributes: + label: Server and Version + placeholder: (e.g. Navidrome v0.48.0) + validations: + required: true + - type: input + attributes: + label: Node Version (if developing locally) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..1ec3dd3ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Question + url: https://github.com/jeffvli/feishin/discussions + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..2043e1af7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,22 @@ +name: Feature request +description: Request a feature to be added to Feishin 🎉 +labels: ['enhancement'] +body: + - type: textarea + attributes: + label: What do you want to be added? + validations: + required: true + - type: textarea + attributes: + label: Additional context + validations: + required: false + - type: checkboxes + attributes: + label: Is this a server-specific feature? (e.g. Jellyfin only) + options: + - label: 'Yes' + required: false + validations: + required: false diff --git a/README.md b/README.md index a4a286520..a0218754d 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,20 @@ First thing to do is check that your MPV binary path is correct. Navigate to the Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/) or [Jellyfin](https://jellyfin.org/) API. **Subsonic API is not currently supported**. This will likely be added in [later when the new Subsonic API is decided on](https://support.symfonium.app/t/subsonic-servers-participation/1233). -- [Navidrome](https://github.com/navidrome/navidrome) version 0.48.0 and newer +- [Navidrome](https://github.com/navidrome/navidrome) - [Jellyfin](https://github.com/jellyfin/jellyfin) - [Funkwhale](https://funkwhale.audio/) - TBD - Subsonic-compatible servers - TBD +### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux + +This happens when you have user (unprivileged) namespaces disabled (`sysctl kernel.unprivileged_userns_clone` returns 0). You can fix this by either enabling unprivileged namespaces, or by making the `chrome-sandbox` Setuid. + +```bash +chmod 4755 chrome-sandbox +sudo chown root:root chrome-sandbox +``` + ## Development Built and tested using Node `v16.15.0`. diff --git a/docker-compose.yaml b/docker-compose.yaml index 443083810..eadc0b3b1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3.5' services: feishin: container_name: feishin - image: jeffvli/feishin + image: ghcr.io/jeffvli/feishin:latest restart: unless-stopped ports: - 9180:9180 diff --git a/package-lock.json b/package-lock.json index c4f6d6a80..59f1bda9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "electron-updater": "^4.6.5", "fast-average-color": "^9.3.0", "format-duration": "^2.0.0", - "framer-motion": "^10.13.0", + "framer-motion": "^11.0.0", "fuse.js": "^6.6.2", "history": "^5.3.0", "i18next": "^21.10.0", @@ -66,6 +66,7 @@ "react-virtualized-auto-sizer": "^1.0.17", "react-window": "^1.8.9", "react-window-infinite-loader": "^1.0.9", + "sanitize-html": "^2.13.0", "semver": "^7.5.4", "styled-components": "^6.0.8", "swiper": "^9.3.1", @@ -90,6 +91,7 @@ "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "@types/react-window-infinite-loader": "^1.0.6", + "@types/sanitize-html": "^2.11.0", "@types/styled-components": "^5.1.26", "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-bundle-analyzer": "^4.4.1", @@ -105,7 +107,7 @@ "css-minimizer-webpack-plugin": "^3.4.1", "detect-port": "^1.3.0", "electron": "^26.6.10", - "electron-builder": "^24.9.0", + "electron-builder": "^24.13.3", "electron-devtools-installer": "^3.2.0", "electron-notarize": "^1.2.1", "electronmon": "^2.0.2", @@ -220,6 +222,8 @@ "integrity": "sha512-prtg5f6zCERIaECeTZzd2fMtVjlfjhUcO+fBLQ6DXXdq5FljN+excVitJ2nogsusdf31LeqkjAfXZ7Xq+HmN8g==", "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0", "commander": "^4.0.1", "convert-source-map": "^1.1.0", "fs-readdir-recursive": "^1.1.0", @@ -2359,9 +2363,9 @@ } }, "node_modules/@electron/asar": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz", - "integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz", + "integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==", "dev": true, "dependencies": { "commander": "^5.0.0", @@ -2384,6 +2388,7 @@ "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", + "global-agent": "^3.0.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", @@ -2406,9 +2411,9 @@ } }, "node_modules/@electron/notarize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz", - "integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", "dev": true, "dependencies": { "debug": "^4.1.1", @@ -2440,6 +2445,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -2508,6 +2514,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -2699,6 +2706,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -2759,6 +2767,7 @@ "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "dev": true, "dependencies": { + "encoding": "^0.1.13", "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" @@ -2908,9 +2917,9 @@ } }, "node_modules/@electron/universal": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", - "integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", "dev": true, "dependencies": { "@electron/asar": "^3.2.1", @@ -2946,6 +2955,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -3223,6 +3233,96 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3730,6 +3830,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -3908,6 +4009,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz", @@ -4505,6 +4616,7 @@ "dependencies": { "camelcase": "^5.3.1", "loader-utils": "^1.4.2", + "prettier": "*", "schema-utils": "^2.0.1" }, "optionalDependencies": { @@ -5110,6 +5222,89 @@ "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, + "node_modules/@types/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, + "node_modules/@types/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -5197,9 +5392,9 @@ } }, "node_modules/@types/verror": { - "version": "1.10.9", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", - "integrity": "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==", + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", "dev": true, "optional": true }, @@ -5952,26 +6147,25 @@ "dev": true }, "node_modules/app-builder-lib": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.0.tgz", - "integrity": "sha512-eqxC5QZQoZzwqBkd9Rd0O3T/VaSOmgW9pgNc+tXrEktpQ56cEFt4s1AaQjGrLSajamXerVj6bZM5yZFp+CCyqA==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", "dev": true, "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.1.0", + "@electron/notarize": "2.2.1", "@electron/osx-sign": "1.0.5", - "@electron/universal": "1.4.1", + "@electron/universal": "1.5.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", - "7zip-bin": "~5.2.0", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", - "electron-publish": "24.8.1", + "electron-publish": "24.13.1", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", @@ -5988,6 +6182,10 @@ }, "engines": { "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" } }, "node_modules/app-builder-lib/node_modules/brace-expansion": { @@ -6000,9 +6198,9 @@ } }, "node_modules/app-builder-lib/node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -6032,6 +6230,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -6077,6 +6276,62 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/are-we-there-yet": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", @@ -6575,21 +6830,21 @@ } }, "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -6640,7 +6895,7 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/bonjour-service": { @@ -6852,16 +7107,16 @@ "dev": true }, "node_modules/builder-util": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz", - "integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==", + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", "dev": true, "dependencies": { "@types/debug": "^4.1.6", "7zip-bin": "~5.2.0", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.3", + "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", @@ -6888,9 +7143,9 @@ } }, "node_modules/builder-util/node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -6920,6 +7175,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -7176,6 +7432,7 @@ "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", + "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -7492,6 +7749,37 @@ "node": ">=0.10.0" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -7637,26 +7925,81 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/config-file-ts": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", - "integrity": "sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", "dev": true, "dependencies": { - "glob": "^7.1.6", - "typescript": "^4.0.2" + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/config-file-ts/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/confusing-browser-globals": { @@ -7713,9 +8056,9 @@ ] }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "engines": { "node": ">= 0.6" @@ -7730,9 +8073,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -7825,6 +8168,48 @@ "buffer": "^5.1.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -8606,14 +8991,15 @@ } }, "node_modules/dmg-builder": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.0.tgz", - "integrity": "sha512-0fQdxPtYQYyjj2BScYGhjG6KHp7kcL4+5+X1Kug3zD7IIS7ROv2PV2H3HgGSh9NtUYeY9FLLPKSfggrzj5ZC4Q==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "dependencies": { - "app-builder-lib": "24.9.0", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "dmg-license": "^1.0.11", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -8623,9 +9009,9 @@ } }, "node_modules/dmg-builder/node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -8655,6 +9041,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -8765,10 +9152,9 @@ } }, "node_modules/domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true, + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", @@ -8915,16 +9301,16 @@ } }, "node_modules/electron-builder": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.0.tgz", - "integrity": "sha512-jA+jYCZlwYzeJEkb82eZNbdMVTUIh99+JQL3yCZjeV3J1N+pdpDrS0P8wZX8vOGpR310TU0tqgjpkxVgE+38tg==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", "dev": true, "dependencies": { - "app-builder-lib": "24.9.0", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", - "dmg-builder": "24.9.0", + "dmg-builder": "24.13.3", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -8940,10 +9326,62 @@ "node": ">=14.0.0" } }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-builder/node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -8987,6 +9425,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -9117,6 +9556,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -9133,14 +9573,14 @@ } }, "node_modules/electron-publish": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz", - "integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==", + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", "dev": true, "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", @@ -9148,9 +9588,9 @@ } }, "node_modules/electron-publish/node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -9180,6 +9620,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -9256,6 +9697,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -9561,7 +10003,8 @@ "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1" + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -10619,17 +11062,17 @@ } }, "node_modules/express": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz", - "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -10645,7 +11088,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -10731,6 +11174,7 @@ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "dependencies": { + "@types/yauzl": "^2.9.1", "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" @@ -11035,9 +11479,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -11053,6 +11497,34 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -11081,20 +11553,22 @@ } }, "node_modules/framer-motion": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.13.0.tgz", - "integrity": "sha512-xKhw9VCizmwEHbopOfluaoVunGHSZyMztGbTvsgOYqCjaKu6qtlwWY1J+6GhL41NY1P157JgEikjDm67XCFnvQ==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.1.7.tgz", + "integrity": "sha512-cW11Pu53eDAXUEhv5hEiWuIXWhfkbV32PlgVISn7jRdcAiVrJ1S03YQQ0/DzoswGYYwKi4qYmHHjCzAH52eSdQ==", + "license": "MIT", "dependencies": { "tslib": "^2.4.0" }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, "peerDependencies": { + "@emotion/is-prop-valid": "*", "react": "^18.0.0", "react-dom": "^18.0.0" }, "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, "react": { "optional": true }, @@ -11103,21 +11577,6 @@ } } }, - "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/framer-motion/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -11127,6 +11586,13 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "peer": true + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -11190,9 +11656,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", "dev": true }, "node_modules/fs-readdir-recursive": { @@ -11360,14 +11826,14 @@ } }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -12226,6 +12692,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -13003,12 +13470,12 @@ "dev": true }, "node_modules/isbinaryfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", - "integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", "dev": true, "engines": { - "node": ">= 14.0.0" + "node": ">= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" @@ -13113,6 +13580,25 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -13375,6 +13861,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^27.5.1", "jest-serializer": "^27.5.1", @@ -13921,6 +14408,9 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -14340,6 +14830,13 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "peer": true + }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", @@ -14351,11 +14848,25 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "peer": true + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "peer": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14374,6 +14885,13 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "peer": true + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -14600,19 +15118,19 @@ "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "dependencies": { - "fs-monkey": "1.0.3" + "fs-monkey": "^1.0.4" }, "engines": { "node": ">= 4.0.0" @@ -15636,6 +16154,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -15713,6 +16236,40 @@ "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=", "dev": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -16761,9 +17318,9 @@ } }, "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -16861,9 +17418,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -17324,6 +17881,39 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -17813,6 +18403,96 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sass": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz", @@ -18588,6 +19268,27 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -18653,6 +19354,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -18839,6 +19553,9 @@ "resolved": "https://registry.npmjs.org/stylelint-config-css-modules/-/stylelint-config-css-modules-4.3.0.tgz", "integrity": "sha512-KvIvhzzjpcjHKkGSPkQCueoZJHrb6sZ6GCtrQb/J45HQTBVwJAeNYXaSZZK6ZQOC7NxJ4v5kLxpQLDiCK6zzgw==", "dev": true, + "dependencies": { + "stylelint-scss": "^5.0.0 || ^6.0.0" + }, "optionalDependencies": { "stylelint-scss": "^5.0.0 || ^6.0.0" }, @@ -19336,6 +20053,38 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tar/node_modules/minipass": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", @@ -19396,6 +20145,7 @@ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^2.0.0" }, "optionalDependencies": { @@ -19567,15 +20317,12 @@ "dev": true }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/tmp-promise": { @@ -20825,13 +21572,13 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.1", + "memfs": "^3.4.3", "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" @@ -21196,6 +21943,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -21350,6 +22115,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/zod": { "version": "3.22.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", @@ -22851,9 +23668,9 @@ "dev": true }, "@electron/asar": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz", - "integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz", + "integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==", "dev": true, "requires": { "commander": "^5.0.0", @@ -22886,9 +23703,9 @@ } }, "@electron/notarize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz", - "integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", "dev": true, "requires": { "debug": "^4.1.1", @@ -23265,9 +24082,9 @@ } }, "@electron/universal": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", - "integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", "dev": true, "requires": { "@electron/asar": "^3.2.1", @@ -23522,6 +24339,65 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -24056,6 +24932,13 @@ "fastq": "^1.6.0" } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.5.tgz", @@ -25028,6 +25911,66 @@ "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, + "@types/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==", + "dev": true, + "requires": { + "htmlparser2": "^8.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -25114,9 +26057,9 @@ } }, "@types/verror": { - "version": "1.10.9", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", - "integrity": "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==", + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", "dev": true, "optional": true }, @@ -25673,26 +26616,25 @@ "dev": true }, "app-builder-lib": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.0.tgz", - "integrity": "sha512-eqxC5QZQoZzwqBkd9Rd0O3T/VaSOmgW9pgNc+tXrEktpQ56cEFt4s1AaQjGrLSajamXerVj6bZM5yZFp+CCyqA==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", "dev": true, "requires": { "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.1.0", + "@electron/notarize": "2.2.1", "@electron/osx-sign": "1.0.5", - "@electron/universal": "1.4.1", + "@electron/universal": "1.5.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", - "7zip-bin": "~5.2.0", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", - "electron-publish": "24.8.1", + "electron-publish": "24.13.1", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", @@ -25718,9 +26660,9 @@ } }, "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "requires": { "debug": "^4.3.4", @@ -25780,6 +26722,55 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, + "archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "peer": true, + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "peer": true, + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + } + }, "are-we-there-yet": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", @@ -26158,21 +27149,21 @@ } }, "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -26210,7 +27201,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } @@ -26367,16 +27358,16 @@ "dev": true }, "builder-util": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz", - "integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==", + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", "dev": true, "requires": { "@types/debug": "^4.1.6", "7zip-bin": "~5.2.0", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.3", + "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", @@ -26391,9 +27382,9 @@ }, "dependencies": { "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "requires": { "debug": "^4.3.4", @@ -26848,6 +27839,33 @@ "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true }, + "compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "peer": true, + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -26965,19 +27983,56 @@ } }, "config-file-ts": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", - "integrity": "sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", "dev": true, "requires": { - "glob": "^7.1.6", - "typescript": "^4.0.2" + "glob": "^10.3.10", + "typescript": "^5.3.3" }, "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true } } @@ -27018,9 +28073,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, "convert-source-map": { @@ -27032,9 +28087,9 @@ } }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true }, "cookie-signature": { @@ -27099,6 +28154,38 @@ "buffer": "^5.1.0" } }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "peer": true + }, + "crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "peer": true, + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -27667,14 +28754,14 @@ } }, "dmg-builder": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.0.tgz", - "integrity": "sha512-0fQdxPtYQYyjj2BScYGhjG6KHp7kcL4+5+X1Kug3zD7IIS7ROv2PV2H3HgGSh9NtUYeY9FLLPKSfggrzj5ZC4Q==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "requires": { - "app-builder-lib": "24.9.0", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "dmg-license": "^1.0.11", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", @@ -27682,9 +28769,9 @@ }, "dependencies": { "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "requires": { "debug": "^4.3.4", @@ -27797,10 +28884,9 @@ } }, "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, "domexception": { "version": "2.0.1", @@ -27920,16 +29006,16 @@ } }, "electron-builder": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.0.tgz", - "integrity": "sha512-jA+jYCZlwYzeJEkb82eZNbdMVTUIh99+JQL3yCZjeV3J1N+pdpDrS0P8wZX8vOGpR310TU0tqgjpkxVgE+38tg==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", "dev": true, "requires": { - "app-builder-lib": "24.9.0", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", - "dmg-builder": "24.9.0", + "dmg-builder": "24.13.3", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -27939,9 +29025,9 @@ }, "dependencies": { "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "requires": { "debug": "^4.3.4", @@ -28009,6 +29095,51 @@ } } }, + "electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "peer": true, + "requires": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "peer": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "peer": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "peer": true + } + } + }, "electron-debug": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/electron-debug/-/electron-debug-3.2.0.tgz", @@ -28097,14 +29228,14 @@ } }, "electron-publish": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz", - "integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==", + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", "dev": true, "requires": { "@types/fs-extra": "^9.0.11", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", @@ -28112,9 +29243,9 @@ }, "dependencies": { "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "requires": { "debug": "^4.3.4", @@ -29190,17 +30321,17 @@ } }, "express": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz", - "integrity": "sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -29216,7 +30347,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -29531,9 +30662,27 @@ } }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } }, "form-data": { "version": "4.0.0", @@ -29557,29 +30706,11 @@ "dev": true }, "framer-motion": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.13.0.tgz", - "integrity": "sha512-xKhw9VCizmwEHbopOfluaoVunGHSZyMztGbTvsgOYqCjaKu6qtlwWY1J+6GhL41NY1P157JgEikjDm67XCFnvQ==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.1.7.tgz", + "integrity": "sha512-cW11Pu53eDAXUEhv5hEiWuIXWhfkbV32PlgVISn7jRdcAiVrJ1S03YQQ0/DzoswGYYwKi4qYmHHjCzAH52eSdQ==", "requires": { - "@emotion/is-prop-valid": "^0.8.2", "tslib": "^2.4.0" - }, - "dependencies": { - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - } } }, "fresh": { @@ -29588,6 +30719,13 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "peer": true + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -29644,9 +30782,9 @@ } }, "fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", "dev": true }, "fs-readdir-recursive": { @@ -29769,14 +30907,14 @@ } }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -30981,9 +32119,9 @@ "dev": true }, "isbinaryfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", - "integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", "dev": true }, "isexe": { @@ -31065,6 +32203,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -32006,6 +33154,13 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "peer": true + }, "lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", @@ -32017,11 +33172,25 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "peer": true + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "peer": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -32040,6 +33209,13 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "peer": true + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -32212,16 +33388,16 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, "memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "requires": { - "fs-monkey": "1.0.3" + "fs-monkey": "^1.0.4" } }, "memoize-one": { @@ -32971,6 +34147,11 @@ "lines-and-columns": "^1.1.6" } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -33036,6 +34217,30 @@ "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=", "dev": true }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + } + } + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -33756,9 +34961,9 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -33826,9 +35031,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "requires": { "bytes": "3.1.2", @@ -34147,6 +35352,38 @@ "util-deprecate": "~1.0.1" } }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "peer": true, + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "peer": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -34521,6 +35758,70 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, "sass": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz", @@ -35146,6 +36447,25 @@ } } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + } + } + }, "string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -35193,6 +36513,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -35711,6 +37040,34 @@ } } }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "peer": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -35882,13 +37239,10 @@ "dev": true }, "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true }, "tmp-promise": { "version": "3.0.3", @@ -36810,13 +38164,13 @@ } }, "webpack-dev-middleware": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "requires": { "colorette": "^2.0.10", - "memfs": "^3.4.1", + "memfs": "^3.4.3", "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" @@ -37072,6 +38426,17 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -37185,6 +38550,51 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "peer": true, + "requires": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "peer": true, + "requires": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "zod": { "version": "3.22.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", diff --git a/package.json b/package.json index 5405484f9..82e38630f 100644 --- a/package.json +++ b/package.json @@ -216,6 +216,7 @@ "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "@types/react-window-infinite-loader": "^1.0.6", + "@types/sanitize-html": "^2.11.0", "@types/styled-components": "^5.1.26", "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-bundle-analyzer": "^4.4.1", @@ -231,7 +232,7 @@ "css-minimizer-webpack-plugin": "^3.4.1", "detect-port": "^1.3.0", "electron": "^26.6.10", - "electron-builder": "^24.9.0", + "electron-builder": "^24.13.3", "electron-devtools-installer": "^3.2.0", "electron-notarize": "^1.2.1", "electronmon": "^2.0.2", @@ -318,7 +319,7 @@ "electron-updater": "^4.6.5", "fast-average-color": "^9.3.0", "format-duration": "^2.0.0", - "framer-motion": "^10.13.0", + "framer-motion": "^11.0.0", "fuse.js": "^6.6.2", "history": "^5.3.0", "i18next": "^21.10.0", @@ -345,6 +346,7 @@ "react-virtualized-auto-sizer": "^1.0.17", "react-window": "^1.8.9", "react-window-infinite-loader": "^1.0.9", + "sanitize-html": "^2.13.0", "semver": "^7.5.4", "styled-components": "^6.0.8", "swiper": "^9.3.1", diff --git a/release/app/package-lock.json b/release/app/package-lock.json index 8c242cf43..0acd09bba 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -27,6 +27,7 @@ "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", + "global-agent": "^3.0.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", @@ -294,6 +295,7 @@ "integrity": "sha512-tzQq/+wrTZ2yU+U5PoeXc97KABhX2v55C/T0finH3tSKYuI8H/SqppIFymBBrUHcK13LvEGY3vdj3ikPPenL5g==", "dependencies": { "@nornagon/put": "0.0.8", + "abstract-socket": "^2.0.0", "event-stream": "3.3.4", "hexy": "^0.2.10", "jsbi": "^2.0.5", @@ -539,6 +541,7 @@ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "dependencies": { + "@types/yauzl": "^2.9.1", "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" @@ -868,6 +871,9 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } diff --git a/src/i18n/locales/cs.json b/src/i18n/locales/cs.json index 826bc696c..0afd5ae53 100644 --- a/src/i18n/locales/cs.json +++ b/src/i18n/locales/cs.json @@ -365,7 +365,8 @@ "albumArtist": "umělec alba", "path": "cesta", "discNumber": "disk", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" } }, "error": { diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 8b1309dbc..618a4f60b 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -310,7 +310,8 @@ "discNumber": "Disk", "genre": "$t(entity.genre_one)", "songCount": "$t(entity.track_other)", - "trackNumber": "Nr." + "trackNumber": "Nr.", + "size": "$t(common.size)" } }, "page": { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7591b29d5..ad60bd42c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -26,6 +26,8 @@ "action_one": "action", "action_other": "actions", "add": "add", + "albumGain": "album gain", + "albumPeak": "album peak", "areYouSure": "are you sure?", "ascending": "ascending", "backward": "backward", @@ -72,6 +74,7 @@ "menu": "menu", "minimize": "minimize", "modified": "modified", + "mbid": "MusicBrainz ID", "name": "name", "no": "no", "none": "none", @@ -98,10 +101,13 @@ "setting": "setting", "setting_one": "setting", "setting_other": "settings", + "share": "share", "size": "size", "sortOrder": "order", "title": "title", "trackNumber": "track", + "trackGain": "track gain", + "trackPeak": "track peak", "unknown": "unknown", "version": "version", "year": "year", @@ -153,6 +159,7 @@ "loginRateError": "too many login attempts, please try again in a few seconds", "mpvRequired": "MPV required", "networkError": "a network error occurred", + "openError": "could not open file", "playbackError": "an error occurred when trying to play the media", "remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server", "remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server", @@ -252,6 +259,14 @@ "input_optionMatchAll": "match all", "input_optionMatchAny": "match any" }, + "shareItem": { + "allowDownloading": "allow downloading", + "description": "description", + "setExpiration": "set expiration", + "success": "share link copied to clipboard (or click here to open)", + "expireInvalid": "expiration must be in the future", + "createFailed": "failed to create share (is sharing enabled?)" + }, "updateServer": { "success": "server updated successfully", "title": "update server" @@ -277,6 +292,8 @@ "moreFromGeneric": "more from {{item}}" }, "albumList": { + "artistAlbums": "Albums by {{artist}}", + "genreAlbums": "\"{{genre}}\" $t(entity.album_other)", "title": "$t(entity.album_other)" }, "appMenu": { @@ -307,7 +324,9 @@ "removeFromFavorites": "$t(action.removeFromFavorites)", "removeFromPlaylist": "$t(action.removeFromPlaylist)", "removeFromQueue": "$t(action.removeFromQueue)", - "setRating": "$t(action.setRating)" + "setRating": "$t(action.setRating)", + "shareItem": "share item", + "showDetails": "get info" }, "fullscreenPlayer": { "config": { @@ -330,6 +349,8 @@ "upNext": "up next" }, "genreList": { + "showAlbums": "show $t(entity.genre_one) $t(entity.album_other)", + "showTracks": "show $t(entity.genre_one) $t(entity.track_other)", "title": "$t(entity.genre_other)" }, "globalSearch": { @@ -347,6 +368,11 @@ "recentlyPlayed": "recently played", "title": "$t(common.home)" }, + "itemDetail": { + "copyPath": "copy path to clipboard", + "copiedPath": "path copied successfully", + "openFile": "show track in file manager" + }, "playlistList": { "title": "$t(entity.playlist_other)" }, @@ -463,6 +489,8 @@ "gaplessAudio": "gapless audio", "gaplessAudio_description": "sets the gapless audio setting for mpv", "gaplessAudio_optionWeak": "weak (recommended)", + "genreBehavior": "genre page default behavior", + "genreBehavior_description": "determines whether clicking on a genre opens by default in track or album list", "globalMediaHotkeys": "global media hotkeys", "globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback", "homeConfiguration": "home page configuration", @@ -611,6 +639,7 @@ "rating": "rating", "releaseDate": "release date", "releaseYear": "year", + "size": "$t(common.size)", "songCount": "$t(entity.track_other)", "title": "title", "trackNumber": "track" diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index d463414b7..62e92ac1a 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -541,7 +541,8 @@ "albumArtist": "artista de álbum", "path": "ruta", "discNumber": "disco", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" }, "config": { "label": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 25cfdc686..0d126815a 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -625,7 +625,8 @@ "artist": "$t(entity.artist_one)", "genre": "$t(entity.genre_one)", "songCount": "$t(entity.track_other)", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" } } } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 87b1fec62..a6cf75ea5 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -572,7 +572,8 @@ "albumArtist": "artista album", "path": "percorso", "discNumber": "disco", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" } }, "entity": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 4a84464e9..de959b08f 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -353,7 +353,8 @@ "albumArtist": "アルバムアーティスト", "path": "パス", "discNumber": "ディスク", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" } }, "error": { diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index f23e8bda0..2198c04e1 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -249,7 +249,8 @@ }, "table": { "column": { - "rating": "rating" + "rating": "rating", + "size": "$t(common.size)" }, "config": { "label": { diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index 22793599f..e2c64b287 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -625,7 +625,8 @@ "albumArtist": "artysta albumu", "path": "ścieżka", "discNumber": "płyta", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" } } } diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 56446a481..78c84ee63 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -139,7 +139,8 @@ }, "column": { "title": "titulo", - "discNumber": "disco" + "discNumber": "disco", + "size": "$t(common.size)" } }, "page": { diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 5c008ee72..892bdcf67 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -204,7 +204,8 @@ "trackNumber": "трек", "genre": "$t(entity.genre_one)", "path": "путь", - "discNumber": "диск" + "discNumber": "диск", + "size": "$t(common.size)" } }, "error": { diff --git a/src/i18n/locales/sr.json b/src/i18n/locales/sr.json index 7a146fea9..b038516d3 100644 --- a/src/i18n/locales/sr.json +++ b/src/i18n/locales/sr.json @@ -359,7 +359,8 @@ "albumArtist": "album artist", "path": "putanja", "discNumber": "disk", - "channels": "$t(common.channel_other)" + "channels": "$t(common.channel_other)", + "size": "$t(common.size)" } }, "error": { diff --git a/src/i18n/locales/zh-Hans.json b/src/i18n/locales/zh-Hans.json index f8d9e2c28..452f0a862 100644 --- a/src/i18n/locales/zh-Hans.json +++ b/src/i18n/locales/zh-Hans.json @@ -595,7 +595,8 @@ "albumArtist": "专辑艺术家", "path": "路径", "channels": "$t(common.channel_other)", - "discNumber": "盘" + "discNumber": "盘", + "size": "$t(common.size)" } } } diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 2ac9c349b..6d35dee55 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -463,7 +463,8 @@ "bpm": "bpm", "songCount": "$t(entity.track_other)", "title": "標題", - "trackNumber": "音軌編號" + "trackNumber": "音軌編號", + "size": "$t(common.size)" } }, "action": { diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index f6d6f4b3e..47065792c 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -180,7 +180,11 @@ ipcMain.handle( // Clean up previous mpv instance getMpvInstance()?.stop(); - getMpvInstance()?.quit(); + getMpvInstance() + ?.quit() + .catch((error) => { + mpvLog({ action: 'Failed to quit existing MPV' }, error); + }); mpvInstance = null; mpvInstance = await createMpv(data); @@ -211,11 +215,12 @@ ipcMain.handle( ipcMain.on('player-quit', async () => { try { - getMpvInstance()?.stop(); - getMpvInstance()?.quit(); - mpvInstance = null; + await getMpvInstance()?.stop(); + await getMpvInstance()?.quit(); } catch (err: NodeMpvError | any) { mpvLog({ action: 'Failed to quit mpv' }, err); + } finally { + mpvInstance = null; } }); @@ -301,7 +306,7 @@ ipcMain.on('player-seek-to', async (_event, time: number) => { // Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => { - if (!data.queue.current && !data.queue.next) { + if (!data.queue.current?.id && !data.queue.next?.id) { try { await getMpvInstance()?.clearPlaylist(); await getMpvInstance()?.pause(); @@ -312,14 +317,14 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) } try { - if (data.queue.current) { + if (data.queue.current?.streamUrl) { await getMpvInstance() ?.load(data.queue.current.streamUrl, 'replace') .catch(() => { getMpvInstance()?.play(); }); - if (data.queue.next) { + if (data.queue.next?.streamUrl) { await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } } @@ -348,7 +353,7 @@ ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => { await getMpvInstance()?.playlistRemove(1); } - if (data.queue.next) { + if (data.queue.next?.streamUrl) { await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } } catch (err: NodeMpvError | any) { @@ -368,7 +373,7 @@ ipcMain.on('player-auto-next', async (_event, data: PlayerData) => { getMpvInstance()?.pause(); }); - if (data.queue.next) { + if (data.queue.next?.streamUrl) { await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } } catch (err: NodeMpvError | any) { @@ -407,11 +412,19 @@ ipcMain.handle('player-get-time', async (): Promise => { } }); -app.on('before-quit', () => { - getMpvInstance()?.stop(); - getMpvInstance()?.quit(); +app.on('before-quit', async () => { + try { + await getMpvInstance()?.stop(); + await getMpvInstance()?.quit(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to cleanly before-quit` }, err); + } }); -app.on('window-all-closed', () => { - getMpvInstance()?.quit(); +app.on('window-all-closed', async () => { + try { + await getMpvInstance()?.quit(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to cleanly exit` }, err); + } }); diff --git a/src/main/features/core/player/media-keys.ts b/src/main/features/core/player/media-keys.ts index bac8c2ca1..b87f9ee1c 100644 --- a/src/main/features/core/player/media-keys.ts +++ b/src/main/features/core/player/media-keys.ts @@ -6,18 +6,20 @@ import { store } from '../settings'; export const enableMediaKeys = (window: BrowserWindow | null) => { if (isMacOS()) { const shouldPrompt = store.get('should_prompt_accessibility', true) as boolean; + const shownWarning = store.get('shown_accessibility_warning', false) as boolean; const trusted = systemPreferences.isTrustedAccessibilityClient(shouldPrompt); if (shouldPrompt) { store.set('should_prompt_accessibility', false); } - if (!trusted) { + if (!trusted && !shownWarning) { window?.webContents.send('toast-from-main', { message: 'Feishin is not a trusted accessibility client. Media keys will not work until this setting is changed', type: 'warning', }); + store.set('shown_accessibility_warning', true); } } diff --git a/src/main/features/linux/mpris.ts b/src/main/features/linux/mpris.ts index 4fe285d0c..fcdf4ac05 100644 --- a/src/main/features/linux/mpris.ts +++ b/src/main/features/linux/mpris.ts @@ -18,22 +18,29 @@ mprisPlayer.on('quit', () => { process.exit(); }); +const hasData = (): boolean => { + return mprisPlayer.metadata && !!mprisPlayer.metadata['mpris:length']; +}; + mprisPlayer.on('stop', () => { getMainWindow()?.webContents.send('renderer-player-stop'); mprisPlayer.playbackStatus = 'Paused'; }); mprisPlayer.on('pause', () => { + if (!hasData()) return; getMainWindow()?.webContents.send('renderer-player-pause'); mprisPlayer.playbackStatus = 'Paused'; }); mprisPlayer.on('play', () => { + if (!hasData()) return; getMainWindow()?.webContents.send('renderer-player-play'); mprisPlayer.playbackStatus = 'Playing'; }); mprisPlayer.on('playpause', () => { + if (!hasData()) return; getMainWindow()?.webContents.send('renderer-player-play-pause'); if (mprisPlayer.playbackStatus !== 'Playing') { mprisPlayer.playbackStatus = 'Playing'; @@ -43,6 +50,7 @@ mprisPlayer.on('playpause', () => { }); mprisPlayer.on('next', () => { + if (!hasData()) return; getMainWindow()?.webContents.send('renderer-player-next'); if (mprisPlayer.playbackStatus !== 'Playing') { @@ -51,6 +59,7 @@ mprisPlayer.on('next', () => { }); mprisPlayer.on('previous', () => { + if (!hasData()) return; getMainWindow()?.webContents.send('renderer-player-previous'); if (mprisPlayer.playbackStatus !== 'Playing') { @@ -136,7 +145,10 @@ ipcMain.on('update-song', (_event, args: SongUpdate) => { mprisPlayer.shuffle = shuffle; } - if (!song) return; + if (!song) { + mprisPlayer.metadata = {}; + return; + } const upsizedImageUrl = song.imageUrl ? song.imageUrl diff --git a/src/main/main.ts b/src/main/main.ts index 107c6562b..4fc26e1af 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -24,6 +24,8 @@ import { BrowserWindowConstructorOptions, protocol, net, + Rectangle, + screen, } from 'electron'; import electronLocalShortcut from 'electron-localshortcut'; import log from 'electron-log/main'; @@ -256,6 +258,26 @@ const createWindow = async (first = true) => { ...(nativeFrame && isWindows() && nativeFrameConfig.windows), }); + // From https://github.com/electron/electron/issues/526#issuecomment-1663959513 + const bounds = store.get('bounds') as Rectangle | undefined; + if (bounds) { + const screenArea = screen.getDisplayMatching(bounds).workArea; + if ( + bounds.x > screenArea.x + screenArea.width || + bounds.x < screenArea.x || + bounds.y < screenArea.y || + bounds.y > screenArea.y + screenArea.height + ) { + if (bounds.width < screenArea.width && bounds.height < screenArea.height) { + mainWindow.setBounds({ height: bounds.height, width: bounds.width }); + } else { + mainWindow.setBounds({ height: 900, width: 1440 }); + } + } else { + mainWindow.setBounds(bounds); + } + } + electronLocalShortcut.register(mainWindow, 'Ctrl+Shift+I', () => { mainWindow?.webContents.openDevTools(); }); @@ -342,6 +364,20 @@ const createWindow = async (first = true) => { } }); + ipcMain.handle('open-item', async (_event, path: string) => { + return new Promise((resolve, reject) => { + access(path, constants.F_OK, (error) => { + if (error) { + reject(error); + return; + } + + shell.showItemInFolder(path); + resolve(); + }); + }); + }); + const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean; if (globalMediaKeysEnabled) { @@ -358,6 +394,16 @@ const createWindow = async (first = true) => { } if (!first || !startWindowMinimized) { + const maximized = store.get('maximized'); + const fullScreen = store.get('fullscreen'); + + if (maximized) { + mainWindow.maximize(); + } + if (fullScreen) { + mainWindow.setFullScreen(true); + } + mainWindow.show(); createWinThumbarButtons(); } @@ -371,6 +417,10 @@ const createWindow = async (first = true) => { let saved = false; mainWindow.on('close', (event) => { + store.set('bounds', mainWindow?.getNormalBounds()); + store.set('maximized', mainWindow?.isMaximized()); + store.set('fullscreen', mainWindow?.isFullScreen()); + if (!exitFromTray && store.get('window_exit_to_tray')) { if (isMacOS() && !forceQuit) { exitFromTray = true; diff --git a/src/main/preload/local-settings.ts b/src/main/preload/local-settings.ts index c6269411d..5f20e5747 100644 --- a/src/main/preload/local-settings.ts +++ b/src/main/preload/local-settings.ts @@ -64,6 +64,7 @@ const env = { SERVER_NAME: process.env.SERVER_NAME ?? '', SERVER_TYPE, SERVER_URL: process.env.SERVER_URL ?? 'http://', + START_MAXIMIZED: store.get('maximized'), }; export const localSettings = { diff --git a/src/main/preload/utils.ts b/src/main/preload/utils.ts index 12f83ba73..dc4a4132c 100644 --- a/src/main/preload/utils.ts +++ b/src/main/preload/utils.ts @@ -10,6 +10,10 @@ const restoreQueue = () => { ipcRenderer.send('player-restore-queue'); }; +const openItem = async (path: string) => { + return ipcRenderer.invoke('open-item', path); +}; + const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => { ipcRenderer.on('renderer-save-queue', cb); }; @@ -51,6 +55,7 @@ export const utils = { mainMessageListener, onRestoreQueue, onSaveQueue, + openItem, playerErrorListener, restoreQueue, saveQueue, diff --git a/src/remote/components/remote-container.tsx b/src/remote/components/remote-container.tsx index 2213fcbb3..496bb6105 100644 --- a/src/remote/components/remote-container.tsx +++ b/src/remote/components/remote-container.tsx @@ -61,6 +61,7 @@ export const RemoteContainer = () => { spacing={0} > send({ event: 'previous' })} @@ -68,7 +69,8 @@ export const RemoteContainer = () => { { if (status === PlayerStatus.PLAYING) { @@ -78,13 +80,14 @@ export const RemoteContainer = () => { } }} > - {status === PlayerStatus.PLAYING ? ( + {song && status === PlayerStatus.PLAYING ? ( ) : ( )} send({ event: 'next' })} diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index de76e7211..6face864b 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -8,6 +8,7 @@ import type { AlbumArtistDetailArgs, AlbumArtistListArgs, SetRatingArgs, + ShareItemArgs, GenreListArgs, CreatePlaylistArgs, DeletePlaylistArgs, @@ -55,6 +56,7 @@ import type { SimilarSongsArgs, Song, ServerType, + ShareItemResponse, } from '/@/renderer/api/types'; import { DeletePlaylistResponse, RandomSongListArgs } from './types'; import { ndController } from '/@/renderer/api/navidrome/navidrome-controller'; @@ -102,6 +104,7 @@ export type ControllerEndpoint = Partial<{ scrobble: (args: ScrobbleArgs) => Promise; search: (args: SearchArgs) => Promise; setRating: (args: SetRatingArgs) => Promise; + shareItem: (args: ShareItemArgs) => Promise; updatePlaylist: (args: UpdatePlaylistArgs) => Promise; }>; @@ -149,6 +152,7 @@ const endpoints: ApiController = { scrobble: jfController.scrobble, search: jfController.search, setRating: undefined, + shareItem: undefined, updatePlaylist: jfController.updatePlaylist, }, navidrome: { @@ -178,7 +182,7 @@ const endpoints: ApiController = { getPlaylistSongList: ndController.getPlaylistSongList, getRandomSongList: ssController.getRandomSongList, getServerInfo: ndController.getServerInfo, - getSimilarSongs: ssController.getSimilarSongs, + getSimilarSongs: ndController.getSimilarSongs, getSongDetail: ndController.getSongDetail, getSongList: ndController.getSongList, getStructuredLyrics: ssController.getStructuredLyrics, @@ -188,6 +192,7 @@ const endpoints: ApiController = { scrobble: ssController.scrobble, search: ssController.search3, setRating: ssController.setRating, + shareItem: ndController.shareItem, updatePlaylist: ndController.updatePlaylist, }, subsonic: { @@ -223,6 +228,7 @@ const endpoints: ApiController = { scrobble: ssController.scrobble, search: ssController.search3, setRating: undefined, + shareItem: undefined, updatePlaylist: undefined, }, }; @@ -457,6 +463,15 @@ const updateRating = async (args: SetRatingArgs) => { )?.(args); }; +const shareItem = async (args: ShareItemArgs) => { + return ( + apiController( + 'shareItem', + args.apiClientProps.server?.type, + ) as ControllerEndpoint['shareItem'] + )?.(args); +}; + const getTopSongList = async (args: TopSongListArgs) => { return ( apiController( @@ -555,6 +570,7 @@ export const controller = { removeFromPlaylist, scrobble, search, + shareItem, updatePlaylist, updateRating, }; diff --git a/src/renderer/api/features-types.ts b/src/renderer/api/features-types.ts index 779c846eb..f1ccc3e76 100644 --- a/src/renderer/api/features-types.ts +++ b/src/renderer/api/features-types.ts @@ -4,6 +4,7 @@ export enum ServerFeature { LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured', LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured', PLAYLISTS_SMART = 'playlistsSmart', + SHARING_ALBUM_SONG = 'sharingAlbumSong', } export type ServerFeatures = Partial>; diff --git a/src/renderer/api/jellyfin.types.ts b/src/renderer/api/jellyfin.types.ts index 0066186be..dae3f8d3d 100644 --- a/src/renderer/api/jellyfin.types.ts +++ b/src/renderer/api/jellyfin.types.ts @@ -574,7 +574,7 @@ export enum JFSongListSort { ARTIST = 'Artist,Album,SortName', COMMUNITY_RATING = 'CommunityRating,SortName', DURATION = 'Runtime,AlbumArtist,Album,SortName', - NAME = 'Name,SortName', + NAME = 'SortName,Name', PLAY_COUNT = 'PlayCount,SortName', RANDOM = 'Random,SortName', RECENTLY_ADDED = 'DateCreated,SortName', @@ -601,7 +601,7 @@ export type JFSongListParams = { export enum JFAlbumArtistListSort { ALBUM = 'Album,SortName', DURATION = 'Runtime,AlbumArtist,Album,SortName', - NAME = 'Name,SortName', + NAME = 'SortName,Name', RANDOM = 'Random,SortName', RECENTLY_ADDED = 'DateCreated,SortName', RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName', @@ -618,7 +618,7 @@ export type JFAlbumArtistListParams = { export enum JFArtistListSort { ALBUM = 'Album,SortName', DURATION = 'Runtime,AlbumArtist,Album,SortName', - NAME = 'Name,SortName', + NAME = 'SortName,Name', RANDOM = 'Random,SortName', RECENTLY_ADDED = 'DateCreated,SortName', RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName', diff --git a/src/renderer/api/jellyfin/jellyfin-api.ts b/src/renderer/api/jellyfin/jellyfin-api.ts index 0ad28a34f..5dbae8a74 100644 --- a/src/renderer/api/jellyfin/jellyfin-api.ts +++ b/src/renderer/api/jellyfin/jellyfin-api.ts @@ -115,6 +115,15 @@ export const contract = c.router({ 400: jfType._response.error, }, }, + getInstantMix: { + method: 'GET', + path: 'songs/:itemId/InstantMix', + query: jfType._parameters.similarSongs, + responses: { + 200: jfType._response.songList, + 400: jfType._response.error, + }, + }, getMusicFolderList: { method: 'GET', path: 'users/:userId/items', @@ -204,7 +213,7 @@ export const contract = c.router({ }, getSongLyrics: { method: 'GET', - path: 'users/:userId/Items/:id/Lyrics', + path: 'audio/:id/Lyrics', responses: { 200: jfType._response.lyrics, 404: jfType._response.error, diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 31082b03e..2958dad0a 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -61,7 +61,8 @@ import packageJson from '../../../../package.json'; import { z } from 'zod'; import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; import isElectron from 'is-electron'; -import { ServerFeatures } from '/@/renderer/api/features-types'; +import { ServerFeature } from '/@/renderer/api/features-types'; +import { VersionInfo, getFeatures } from '/@/renderer/api/utils'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -231,7 +232,7 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { throw new Error('Failed to get song list'); } + let items: z.infer[]; + + // Jellyfin Bodge because of code from https://github.com/jellyfin/jellyfin/blob/c566ccb63bf61f9c36743ddb2108a57c65a2519b/Emby.Server.Implementations/Data/SqliteItemRepository.cs#L3622 + // If the Album ID filter is passed, Jellyfin will search for + // 1. the matching album id + // 2. An album with the name of the album. + // It is this second condition causing issues, + if (query.albumIds) { + const albumIdSet = new Set(query.albumIds); + items = res.body.Items.filter((item) => albumIdSet.has(item.AlbumId)); + + if (items.length < res.body.Items.length) { + res.body.TotalRecordCount -= res.body.Items.length - items.length; + } + } else { + items = res.body.Items; + } + return { - items: res.body.Items.map((item) => + items: items.map((item) => jfNormalize.song(item, apiClientProps.server, '', query.imageSize), ), startIndex: query.startIndex, @@ -919,7 +938,6 @@ const getLyrics = async (args: LyricsArgs): Promise => { const res = await jfApiClient(apiClientProps).getSongLyrics({ params: { id: query.songId, - userId: apiClientProps.server?.userId, }, }); @@ -951,6 +969,8 @@ const getSongDetail = async (args: SongDetailArgs): Promise return jfNormalize.song(res.body, apiClientProps.server, ''); }; +const VERSION_INFO: VersionInfo = [['10.9.0', { [ServerFeature.LYRICS_SINGLE_STRUCTURED]: [1] }]]; + const getServerInfo = async (args: ServerInfoArgs): Promise => { const { apiClientProps } = args; @@ -960,9 +980,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to get server info'); } - const features: ServerFeatures = { - lyricsSingleStructured: true, - }; + const features = getFeatures(VERSION_INFO, res.body.Version); return { features, @@ -974,6 +992,8 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { const getSimilarSongs = async (args: SimilarSongsArgs): Promise => { const { apiClientProps, query } = args; + // Prefer getSimilarSongs, where possible. Fallback to InstantMix + // where no similar songs were found. const res = await jfApiClient(apiClientProps).getSimilarSongs({ params: { itemId: query.songId, @@ -985,11 +1005,36 @@ const getSimilarSongs = async (args: SimilarSongsArgs): Promise => { }, }); - if (res.status !== 200) { + if (res.status === 200 && res.body.Items.length) { + const results = res.body.Items.reduce((acc, song) => { + if (song.Id !== query.songId) { + acc.push(jfNormalize.song(song, apiClientProps.server, '')); + } + + return acc; + }, []); + + if (results.length > 0) { + return results; + } + } + + const mix = await jfApiClient(apiClientProps).getInstantMix({ + params: { + itemId: query.songId, + }, + query: { + Fields: 'Genres, DateCreated, MediaSources, ParentId', + Limit: query.count, + UserId: apiClientProps.server?.userId || undefined, + }, + }); + + if (mix.status !== 200) { throw new Error('Failed to get similar songs'); } - return res.body.Items.reduce((acc, song) => { + return mix.body.Items.reduce((acc, song) => { if (song.Id !== query.songId) { acc.push(jfNormalize.song(song, apiClientProps.server, '')); } diff --git a/src/renderer/api/jellyfin/jellyfin-types.ts b/src/renderer/api/jellyfin/jellyfin-types.ts index 302520d58..3790a21d5 100644 --- a/src/renderer/api/jellyfin/jellyfin-types.ts +++ b/src/renderer/api/jellyfin/jellyfin-types.ts @@ -514,7 +514,7 @@ const albumList = pagination.extend({ const albumArtistListSort = { ALBUM: 'Album,SortName', DURATION: 'Runtime,AlbumArtist,Album,SortName', - NAME: 'Name,SortName', + NAME: 'SortName,Name', RANDOM: 'Random,SortName', RECENTLY_ADDED: 'DateCreated,SortName', RELEASE_DATE: 'PremiereDate,AlbumArtist,Album,SortName', @@ -544,7 +544,7 @@ const songListSort = { ARTIST: 'Artist,Album,SortName', COMMUNITY_RATING: 'CommunityRating,SortName', DURATION: 'Runtime,AlbumArtist,Album,SortName', - NAME: 'Name,SortName', + NAME: 'SortName,Name', PLAY_COUNT: 'PlayCount,SortName', RANDOM: 'Random,SortName', RECENTLY_ADDED: 'DateCreated,SortName', diff --git a/src/renderer/api/navidrome.types.ts b/src/renderer/api/navidrome.types.ts index 8e276125e..b9c8c688c 100644 --- a/src/renderer/api/navidrome.types.ts +++ b/src/renderer/api/navidrome.types.ts @@ -242,6 +242,7 @@ export enum NDSongListSort { ID = 'id', PLAY_COUNT = 'playCount', PLAY_DATE = 'playDate', + RANDOM = 'random', RATING = 'rating', RECENTLY_ADDED = 'createdAt', TITLE = 'title', diff --git a/src/renderer/api/navidrome/navidrome-api.ts b/src/renderer/api/navidrome/navidrome-api.ts index b9eb93f9f..535a1535d 100644 --- a/src/renderer/api/navidrome/navidrome-api.ts +++ b/src/renderer/api/navidrome/navidrome-api.ts @@ -157,6 +157,16 @@ export const contract = c.router({ 500: resultWithHeaders(ndType._response.error), }, }, + shareItem: { + body: ndType._parameters.shareItem, + method: 'POST', + path: 'share', + responses: { + 200: resultWithHeaders(ndType._response.shareItem), + 404: resultWithHeaders(ndType._response.error), + 500: resultWithHeaders(ndType._response.error), + }, + }, updatePlaylist: { body: ndType._parameters.updatePlaylist, method: 'PUT', diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 33eb367ae..a74c2d45a 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -1,9 +1,7 @@ import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api'; import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize'; -import { NavidromeExtensions, ndType } from '/@/renderer/api/navidrome/navidrome-types'; +import { ndType } from '/@/renderer/api/navidrome/navidrome-types'; import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; -import semverCoerce from 'semver/functions/coerce'; -import semverGte from 'semver/functions/gte'; import { AlbumArtistDetailArgs, AlbumArtistDetailResponse, @@ -47,10 +45,16 @@ import { genreListSortMap, ServerInfo, ServerInfoArgs, + ShareItemArgs, + ShareItemResponse, + SimilarSongsArgs, + Song, } from '../types'; -import { hasFeature } from '/@/renderer/api/utils'; +import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils'; import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types'; import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types'; +import { NDSongListSort } from '/@/renderer/api/navidrome.types'; +import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize'; const authenticate = async ( url: string, @@ -151,20 +155,18 @@ const getAlbumArtistDetail = async ( throw new Error('Server is required'); } + // Prefer images from getArtistInfo first (which should be proxied) + // Prioritize large > medium > small return ndNormalize.albumArtist( { ...res.body.data, ...(artistInfoRes.status === 200 && { + largeImageUrl: + artistInfoRes.body.artistInfo.largeImageUrl || + artistInfoRes.body.artistInfo.mediumImageUrl || + artistInfoRes.body.artistInfo.smallImageUrl || + res.body.data.largeImageUrl, similarArtists: artistInfoRes.body.artistInfo.similarArtist, - ...(!res.body.data.largeImageUrl && { - largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl, - }), - ...(!res.body.data.mediumImageUrl && { - largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl, - }), - ...(!res.body.data.smallImageUrl && { - largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl, - }), }), }, apiClientProps.server, @@ -482,34 +484,11 @@ const removeFromPlaylist = async ( return null; }; -const VERSION_INFO: Array<[string, Record]> = [ +const VERSION_INFO: VersionInfo = [ + ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }], ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }], ]; -const getFeatures = (version: string): Record => { - const cleanVersion = semverCoerce(version); - const features: Record = {}; - let matched = cleanVersion === null; - - for (const [version, supportedFeatures] of VERSION_INFO) { - if (!matched) { - matched = semverGte(cleanVersion!, version); - } - - if (matched) { - for (const [feature, feat] of Object.entries(supportedFeatures)) { - if (feature in features) { - features[feature].push(...feat); - } else { - features[feature] = feat; - } - } - } - } - - return features; -}; - const getServerInfo = async (args: ServerInfoArgs): Promise => { const { apiClientProps } = args; @@ -520,7 +499,10 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { throw new Error('Failed to ping server'); } - const navidromeFeatures: Record = getFeatures(ping.body.serverVersion!); + const navidromeFeatures: Record = getFeatures( + VERSION_INFO, + ping.body.serverVersion!, + ); if (ping.body.openSubsonic) { const res = await ssApiClient(apiClientProps).getServerInfo(); @@ -541,12 +523,87 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { const features: ServerFeatures = { lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS], - playlistsSmart: !!navidromeFeatures[NavidromeExtensions.SMART_PLAYLISTS], + playlistsSmart: !!navidromeFeatures[ServerFeature.PLAYLISTS_SMART], + sharingAlbumSong: !!navidromeFeatures[ServerFeature.SHARING_ALBUM_SONG], }; return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; }; +const shareItem = async (args: ShareItemArgs): Promise => { + const { body, apiClientProps } = args; + + const res = await ndApiClient(apiClientProps).shareItem({ + body: { + description: body.description, + downloadable: body.downloadable, + expires: body.expires, + resourceIds: body.resourceIds, + resourceType: body.resourceType, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to share item'); + } + + return { + id: res.body.data.id, + }; +}; + +const getSimilarSongs = async (args: SimilarSongsArgs): Promise => { + const { apiClientProps, query } = args; + + // Prefer getSimilarSongs (which queries last.fm) where available + // otherwise find other tracks by the same album artist + const res = await ssApiClient({ + ...apiClientProps, + silent: true, + }).getSimilarSongs({ + query: { + count: query.count, + id: query.songId, + }, + }); + + if (res.status === 200 && res.body.similarSongs?.song) { + const similar = res.body.similarSongs.song.reduce((acc, song) => { + if (song.id !== query.songId) { + acc.push(ssNormalize.song(song, apiClientProps.server, '')); + } + + return acc; + }, []); + + if (similar.length > 0) { + return similar; + } + } + + const fallback = await ndApiClient(apiClientProps).getSongList({ + query: { + _end: 50, + _order: 'ASC', + _sort: NDSongListSort.RANDOM, + _start: 0, + album_artist_id: query.albumArtistIds, + }, + }); + + if (fallback.status !== 200) { + throw new Error('Failed to get similar songs'); + } + + return fallback.body.data.reduce((acc, song) => { + if (song.id !== query.songId) { + acc.push(ndNormalize.song(song, apiClientProps.server, '')); + } + + return acc; + }, []); +}; + export const ndController = { addToPlaylist, authenticate, @@ -561,9 +618,11 @@ export const ndController = { getPlaylistList, getPlaylistSongList, getServerInfo, + getSimilarSongs, getSongDetail, getSongList, getUserList, removeFromPlaylist, + shareItem, updatePlaylist, }; diff --git a/src/renderer/api/navidrome/navidrome-normalize.ts b/src/renderer/api/navidrome/navidrome-normalize.ts index 9f69b9e18..386c6e34e 100644 --- a/src/renderer/api/navidrome/navidrome-normalize.ts +++ b/src/renderer/api/navidrome/navidrome-normalize.ts @@ -81,7 +81,7 @@ const normalizeSong = ( const imagePlaceholderUrl = null; return { album: item.album, - albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }], + albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }], albumId: item.albumId, artistName: item.artist, artists: [{ id: item.artistId, imageUrl: null, name: item.artist }], diff --git a/src/renderer/api/navidrome/navidrome-types.ts b/src/renderer/api/navidrome/navidrome-types.ts index 6165046a8..f7f587ee9 100644 --- a/src/renderer/api/navidrome/navidrome-types.ts +++ b/src/renderer/api/navidrome/navidrome-types.ts @@ -343,9 +343,17 @@ const removeFromPlaylistParameters = z.object({ id: z.array(z.string()), }); -export enum NavidromeExtensions { - SMART_PLAYLISTS = 'smartPlaylists', -} +const shareItem = z.object({ + id: z.string(), +}); + +const shareItemParameters = z.object({ + description: z.string(), + downloadable: z.boolean(), + expires: z.number(), + resourceIds: z.string(), + resourceType: z.string(), +}); export const ndType = { _enum: { @@ -365,6 +373,7 @@ export const ndType = { genreList: genreListParameters, playlistList: playlistListParameters, removeFromPlaylist: removeFromPlaylistParameters, + shareItem: shareItemParameters, songList: songListParameters, updatePlaylist: updatePlaylistParameters, userList: userListParameters, @@ -386,6 +395,7 @@ export const ndType = { playlistSong, playlistSongList, removeFromPlaylist, + shareItem, song, songList, updatePlaylist, diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index 467c7efcb..7e36d8cbb 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -131,7 +131,6 @@ axiosClient.defaults.paramsSerializer = (params) => { axiosClient.interceptors.response.use( (response) => { const data = response.data; - if (data['subsonic-response'].status !== 'ok') { // Suppress code related to non-linked lastfm or spotify from Navidrome if (data['subsonic-response'].error.code !== 0) { @@ -161,12 +160,24 @@ const parsePath = (fullPath: string) => { }; }; +const silentlyTransformResponse = (data: any) => { + const jsonBody = JSON.parse(data); + const status = jsonBody ? jsonBody['subsonic-response']?.status : undefined; + + if (status && status !== 'ok') { + jsonBody['subsonic-response'].error.code = 0; + } + + return jsonBody; +}; + export const ssApiClient = (args: { server: ServerListItem | null; signal?: AbortSignal; + silent?: boolean; url?: string; }) => { - const { server, url, signal } = args; + const { server, url, signal, silent } = args; return initClient(contract, { api: async ({ path, method, headers, body }) => { @@ -206,6 +217,8 @@ export const ssApiClient = (args: { ...params, }, signal, + // In cases where we have a fallback, don't notify the error + transformResponse: silent ? silentlyTransformResponse : undefined, url: `${baseUrl}/${api}`, }); diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 66301beb9..b6acc9e7c 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -469,7 +469,7 @@ const getSimilarSongs = async (args: SimilarSongsArgs): Promise => { throw new Error('Failed to get similar songs'); } - if (!res.body.similarSongs) { + if (!res.body.similarSongs?.song) { return []; } diff --git a/src/renderer/api/subsonic/subsonic-normalize.ts b/src/renderer/api/subsonic/subsonic-normalize.ts index a775eb3d6..e6eb41ed5 100644 --- a/src/renderer/api/subsonic/subsonic-normalize.ts +++ b/src/renderer/api/subsonic/subsonic-normalize.ts @@ -75,7 +75,13 @@ const normalizeSong = ( discNumber: item.discNumber || 1, discSubtitle: null, duration: item.duration ? item.duration * 1000 : 0, - gain: null, + gain: + item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain) + ? { + album: item.replayGain.albumGain, + track: item.replayGain.trackGain, + } + : null, genres: item.genre ? [ { @@ -94,7 +100,13 @@ const normalizeSong = ( lyrics: null, name: item.title, path: item.path, - peak: null, + peak: + item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak) + ? { + album: item.replayGain.albumPeak, + track: item.replayGain.trackPeak, + } + : null, playCount: item?.playCount || 0, releaseDate: null, releaseYear: item.year ? String(item.year) : null, diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index 86d6877dc..eba145c51 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -53,6 +53,13 @@ const musicFolderList = z.object({ }), }); +const songGain = z.object({ + albumGain: z.number().optional(), + albumPeak: z.number().optional(), + trackGain: z.number().optional(), + trackPeak: z.number().optional(), +}); + const song = z.object({ album: z.string().optional(), albumId: z.string().optional(), @@ -72,6 +79,7 @@ const song = z.object({ parent: z.string(), path: z.string(), playCount: z.number().optional(), + replayGain: songGain.optional(), size: z.number(), starred: z.boolean().optional(), suffix: z.string(), diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index d6d3d957b..78d77d557 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -541,7 +541,7 @@ export const songListSortMap: SongListSortMap = { id: NDSongListSort.ID, name: NDSongListSort.TITLE, playCount: NDSongListSort.PLAY_COUNT, - random: undefined, + random: NDSongListSort.RANDOM, rating: NDSongListSort.RATING, recentlyAdded: NDSongListSort.RECENTLY_ADDED, recentlyPlayed: NDSongListSort.PLAY_DATE, @@ -766,6 +766,19 @@ export type RatingQuery = { export type SetRatingArgs = { query: RatingQuery; serverId?: string } & BaseEndpointArgs; +// Sharing +export type ShareItemResponse = { id: string } | undefined; + +export type ShareItemBody = { + description: string; + downloadable: boolean; + expires: number; + resourceIds: string; + resourceType: string; +}; + +export type ShareItemArgs = { body: ShareItemBody; serverId?: string } & BaseEndpointArgs; + // Add to playlist export type AddToPlaylistResponse = null | undefined; @@ -1170,6 +1183,7 @@ export type StructuredLyric = { } & (StructuredUnsyncedLyric | StructuredSyncedLyric); export type SimilarSongsQuery = { + albumArtistIds: string[]; count?: number; songId: string; }; diff --git a/src/renderer/api/utils.ts b/src/renderer/api/utils.ts index 8f10c869d..3034d5c4f 100644 --- a/src/renderer/api/utils.ts +++ b/src/renderer/api/utils.ts @@ -1,4 +1,6 @@ import { AxiosHeaders } from 'axios'; +import semverCoerce from 'semver/functions/coerce'; +import semverGte from 'semver/functions/gte'; import { z } from 'zod'; import { toast } from '/@/renderer/components'; import { useAuthStore } from '/@/renderer/store'; @@ -47,3 +49,54 @@ export const hasFeature = (server: ServerListItem | null, feature: ServerFeature return server.features[feature] ?? false; }; + +export type VersionInfo = ReadonlyArray<[string, Record]>; + +/** + * Returns the available server features given the version string. + * @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server. + * The first version match will automatically consider the rest matched. + * @example + * ``` + * // The CORRECT way to order + * const VERSION_INFO: VersionInfo = [ + * ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }], + * ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }], + * ]; + * // INCORRECT way to order + * const VERSION_INFO: VersionInfo = [ + * ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }], + * ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }], + * ]; + * ``` + * @param version the version string (SemVer) + * @returns a Record containing the matched features (if any) and their versions + */ +export const getFeatures = ( + versionInfo: VersionInfo, + version: string, +): Record => { + const cleanVersion = semverCoerce(version); + const features: Record = {}; + let matched = cleanVersion === null; + + for (const [version, supportedFeatures] of versionInfo) { + if (!matched) { + matched = semverGte(cleanVersion!, version); + } + + if (matched) { + for (const [feature, feat] of Object.entries(supportedFeatures)) { + if (feature in features) { + features[feature].push(...feat); + } else { + features[feature] = [...feat]; + } + } + } + } + + return features; +}; + +export const SEPARATOR_STRING = ' · '; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index abdbd7906..b0b71eb50 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -3,10 +3,9 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod import { ModuleRegistry } from '@ag-grid-community/core'; import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model'; import { MantineProvider } from '@mantine/core'; -import { ModalsProvider } from '@mantine/modals'; import isElectron from 'is-electron'; import { initSimpleImg } from 'react-simple-img'; -import { BaseContextModal, toast } from './components'; +import { toast } from './components'; import { useTheme } from './hooks'; import { IsUpdatedDialog } from './is-updated-dialog'; import { AppRouter } from './router/app-router'; @@ -20,7 +19,6 @@ import './styles/global.scss'; import { ContextMenuProvider } from '/@/renderer/features/context-menu'; import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { PlayQueueHandlerContext } from '/@/renderer/features/player'; -import { AddToPlaylistContextModal } from '/@/renderer/features/playlists'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store'; import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types'; @@ -246,27 +244,11 @@ export const App = () => { }, }} > - - - - - - - + + + + + ); diff --git a/src/renderer/components/feature-carousel/index.tsx b/src/renderer/components/feature-carousel/index.tsx index 2502db833..7e97c0359 100644 --- a/src/renderer/components/feature-carousel/index.tsx +++ b/src/renderer/components/feature-carousel/index.tsx @@ -14,6 +14,7 @@ import { Badge } from '/@/renderer/components/badge'; import { AppRoute } from '/@/renderer/router/routes'; import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add'; import { Play } from '/@/renderer/types'; +import { usePlayButtonBehavior } from '/@/renderer/store'; const Carousel = styled(motion.div)` position: relative; @@ -114,6 +115,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => { const handlePlayQueueAdd = usePlayQueueAdd(); const [itemIndex, setItemIndex] = useState(0); const [direction, setDirection] = useState(0); + const playType = usePlayButtonBehavior(); const currentItem = data?.[itemIndex]; @@ -222,11 +224,18 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => { id: [currentItem.id], type: LibraryItem.ALBUM, }, - playType: Play.NOW, + playType, }); }} > - {t('player.play', { postProcess: 'titleCase' })} + {t( + playType === Play.NOW + ? 'player.play' + : playType === Play.NEXT + ? 'player.addNext' + : 'player.addLast', + { postProcess: 'titleCase' }, + )} )} + {!displayedCheck && ( + + + + )} diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index d75216ee4..728e06081 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -45,6 +45,7 @@ import { } from '/@/renderer/store/settings.store'; import { Play } from '/@/renderer/types'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; +import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; const isFullWidthRow = (node: RowNode) => { return node.id?.startsWith('disc-'); @@ -81,6 +82,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP const isFocused = useAppFocus(); const currentSong = useCurrentSong(); const { externalLinks } = useGeneralSettings(); + const genreRoute = useGenreRoute(); const columnDefs = useMemo( () => getColumnDefs(tableConfig.columns, false, 'albumDetail'), @@ -389,7 +391,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP component={Link} radius={0} size="md" - to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, { + to={generatePath(genreRoute, { genreId: genre.id, })} variant="outline" diff --git a/src/renderer/features/albums/components/album-list-grid-view.tsx b/src/renderer/features/albums/components/album-list-grid-view.tsx index 05fe39080..86bbe4610 100644 --- a/src/renderer/features/albums/components/album-list-grid-view.tsx +++ b/src/renderer/features/albums/components/album-list-grid-view.tsx @@ -19,10 +19,10 @@ import { } from '/@/renderer/components/virtual-grid'; import { useListContext } from '/@/renderer/context/list-context'; import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { CardRow, ListDisplayType } from '/@/renderer/types'; +import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite'; export const AlbumListGridView = ({ gridRef, itemCount }: any) => { const queryClient = useQueryClient(); @@ -36,33 +36,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => { const scrollOffset = searchParams.get('scrollOffset'); const initialScrollOffset = Number(id ? scrollOffset : grid?.scrollOffset) || 0; - const createFavoriteMutation = useCreateFavorite({}); - const deleteFavoriteMutation = useDeleteFavorite({}); - - const handleFavorite = (options: { - id: string[]; - isFavorite: boolean; - itemType: LibraryItem; - }) => { - const { id, itemType, isFavorite } = options; - if (isFavorite) { - deleteFavoriteMutation.mutate({ - query: { - id, - type: itemType, - }, - serverId: server?.id, - }); - } else { - createFavoriteMutation.mutate({ - query: { - id, - type: itemType, - }, - serverId: server?.id, - }); - } - }; + const handleFavorite = useHandleFavorite({ gridRef, server }); const cardRows = useMemo(() => { const rows: CardRow[] = [ALBUM_CARD_ROWS.name]; diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index 657802b0c..9e5032705 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -1,63 +1,59 @@ -import type { ChangeEvent, MutableRefObject } from 'react'; +import { useEffect, useRef, type ChangeEvent, type MutableRefObject } from 'react'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Flex, Group, Stack } from '@mantine/core'; import debounce from 'lodash/debounce'; import { useTranslation } from 'react-i18next'; -import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh'; import { LibraryItem } from '/@/renderer/api/types'; import { PageHeader, SearchInput } from '/@/renderer/components'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; -import { useListContext } from '/@/renderer/context/list-context'; import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; -import { - AlbumListFilter, - useCurrentServer, - useListStoreActions, - useListStoreByKey, - usePlayButtonBehavior, -} from '/@/renderer/store'; -import { ListDisplayType } from '/@/renderer/types'; +import { AlbumListFilter, useCurrentServer, usePlayButtonBehavior } from '/@/renderer/store'; import { titleCase } from '/@/renderer/utils'; +import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh'; interface AlbumListHeaderProps { + genreId?: string; gridRef: MutableRefObject; itemCount?: number; tableRef: MutableRefObject; title?: string; } -export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumListHeaderProps) => { +export const AlbumListHeader = ({ + genreId, + itemCount, + gridRef, + tableRef, + title, +}: AlbumListHeaderProps) => { const { t } = useTranslation(); const server = useCurrentServer(); - const { setFilter, setTablePagination } = useListStoreActions(); const cq = useContainerQuery(); - const { pageKey, handlePlay } = useListContext(); - const { display, filter } = useListStoreByKey({ key: pageKey }); const playButtonBehavior = usePlayButtonBehavior(); - - const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ + const genreRef = useRef(); + const { filter, handlePlay, refresh, search } = useDisplayRefresh({ + gridRef, itemType: LibraryItem.ALBUM, server, + tableRef, }); const handleSearch = debounce((e: ChangeEvent) => { - const searchTerm = e.target.value === '' ? undefined : e.target.value; - const updatedFilters = setFilter({ - data: { searchTerm }, - itemType: LibraryItem.ALBUM, - key: pageKey, - }) as AlbumListFilter; + const updatedFilters = search(e) as AlbumListFilter; - if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { - handleRefreshTable(tableRef, updatedFilters); - setTablePagination({ data: { currentPage: 0 }, key: pageKey }); - } else { - handleRefreshGrid(gridRef, updatedFilters); - } + refresh(updatedFilters); }, 500); + useEffect(() => { + if (genreRef.current && genreRef.current !== genreId) { + refresh(filter); + } + + genreRef.current = genreId; + }, [filter, genreId, refresh, tableRef]); + return ( { + const { t } = useTranslation(); const gridRef = useRef(null); const tableRef = useRef(null); const server = useCurrentServer(); const [searchParams] = useSearchParams(); - const { albumArtistId } = useParams(); + const { albumArtistId, genreId } = useParams(); const pageKey = albumArtistId ? `albumArtistAlbum` : 'album'; const handlePlayQueueAdd = usePlayQueueAdd(); const customFilters = useMemo(() => { const value = { ...(albumArtistId && { artistIds: [albumArtistId] }), + ...(genreId && { + _custom: { + jellyfin: { + GenreIds: genreId, + }, + navidrome: { + genre_id: genreId, + }, + }, + }), }; if (isEmpty(value)) { @@ -35,13 +49,35 @@ const AlbumListRoute = () => { } return value; - }, [albumArtistId]); + }, [albumArtistId, genreId]); const albumListFilter = useListFilterByKey({ filter: customFilters, key: pageKey, }); + const genreList = useGenreList({ + options: { + cacheTime: 1000 * 60 * 60, + enabled: !!genreId, + }, + query: { + sortBy: GenreListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, + serverId: server?.id, + }); + + const genreTitle = useMemo(() => { + if (!genreList.data) return ''; + const genre = genreList.data.items.find((g) => g.id === genreId); + + if (!genre) return 'Unknown'; + + return genre?.name; + }, [genreId, genreList.data]); + const itemCountCheck = useAlbumList({ options: { cacheTime: 1000 * 60, @@ -98,19 +134,27 @@ const AlbumListRoute = () => { return { customFilters, handlePlay, - id: albumArtistId ?? undefined, + id: albumArtistId ?? genreId, pageKey, }; - }, [albumArtistId, customFilters, handlePlay, pageKey]); + }, [albumArtistId, customFilters, genreId, handlePlay, pageKey]); + + const artist = searchParams.get('artistName'); + const title = artist + ? t('page.albumList.artistAlbums', { artist }) + : genreId + ? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) }) + : undefined; return ( { + const bio = detailQuery?.data?.biography; + + if (!bio) return null; + return sanitize(bio); + }, [detailQuery?.data?.biography]); + const showTopSongs = topSongsQuery?.data?.items?.length; const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false; const mbzId = detailQuery?.data?.mbz; @@ -408,7 +416,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten component={Link} radius="md" size="md" - to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, { + to={generatePath(genrePath, { genreId: genre.id, })} variant="outline" @@ -459,7 +467,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten ) : null} - {showBiography ? ( + {biography ? ( - + ) : null} {showTopSongs ? ( diff --git a/src/renderer/features/artists/components/album-artist-list-grid-view.tsx b/src/renderer/features/artists/components/album-artist-list-grid-view.tsx index d98876847..9ceabbb26 100644 --- a/src/renderer/features/artists/components/album-artist-list-grid-view.tsx +++ b/src/renderer/features/artists/components/album-artist-list-grid-view.tsx @@ -11,6 +11,7 @@ import { AlbumArtistListQuery, AlbumArtistListResponse, AlbumArtistListSort, + ArtistListQuery, LibraryItem, } from '/@/renderer/api/types'; import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components'; @@ -20,6 +21,7 @@ import { usePlayQueueAdd } from '/@/renderer/features/player'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useListStoreActions } from '/@/renderer/store'; import { CardRow, ListDisplayType } from '/@/renderer/types'; +import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite'; interface AlbumArtistListGridViewProps { gridRef: MutableRefObject; @@ -34,6 +36,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG const { pageKey } = useListContext(); const { grid, display, filter } = useListStoreByKey({ key: pageKey }); const { setGrid } = useListStoreActions(); + const handleFavorite = useHandleFavorite({ gridRef, server }); const fetchInitialData = useCallback(() => { const query: Omit = { @@ -70,16 +73,13 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG const fetch = useCallback( async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => { - const queryKey = queryKeys.albumArtists.list( - server?.id || '', - { - ...filter, - }, - { - limit, - startIndex, - }, - ); + const query: ArtistListQuery = { + ...filter, + limit, + startIndex, + }; + + const queryKey = queryKeys.albumArtists.list(server?.id || '', query); const albumArtistsRes = await queryClient.fetchQuery( queryKey, @@ -154,6 +154,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG display={display || ListDisplayType.CARD} fetchFn={fetch} fetchInitialData={fetchInitialData} + handleFavorite={handleFavorite} handlePlayQueueAdd={handlePlayQueueAdd} height={height} initialScrollOffset={grid?.scrollOffset || 0} 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 b2048cab6..2cdac1388 100644 --- a/src/renderer/features/artists/components/album-artist-list-header.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header.tsx @@ -3,8 +3,6 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { Flex, Group, Stack } from '@mantine/core'; import debounce from 'lodash/debounce'; import { useTranslation } from 'react-i18next'; -import { useListContext } from '../../../context/list-context'; -import { useListStoreByKey } from '../../../store/list.store'; import { FilterBar } from '../../shared/components/filter-bar'; import { LibraryItem } from '/@/renderer/api/types'; import { PageHeader, SearchInput } from '/@/renderer/components'; @@ -12,9 +10,8 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters'; import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; -import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; -import { AlbumArtistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store'; -import { ListDisplayType } from '/@/renderer/types'; +import { AlbumArtistListFilter, useCurrentServer } from '/@/renderer/store'; +import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh'; interface AlbumArtistListHeaderProps { gridRef: MutableRefObject; @@ -29,30 +26,18 @@ export const AlbumArtistListHeader = ({ }: AlbumArtistListHeaderProps) => { const { t } = useTranslation(); const server = useCurrentServer(); - const { pageKey } = useListContext(); - const { display, filter } = useListStoreByKey({ key: pageKey }); - const { setFilter, setTablePagination } = useListStoreActions(); const cq = useContainerQuery(); - const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ + const { filter, refresh, search } = useDisplayRefresh({ + gridRef, itemType: LibraryItem.ALBUM_ARTIST, server, + tableRef, }); const handleSearch = debounce((e: ChangeEvent) => { - const searchTerm = e.target.value === '' ? undefined : e.target.value; - const updatedFilters = setFilter({ - data: { searchTerm }, - itemType: LibraryItem.ALBUM_ARTIST, - key: pageKey, - }) as AlbumArtistListFilter; - - if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { - handleRefreshTable(tableRef, updatedFilters); - setTablePagination({ data: { currentPage: 0 }, key: pageKey }); - } else { - handleRefreshGrid(gridRef, updatedFilters); - } + const updatedFilters = search(e) as AlbumArtistListFilter; + refresh(updatedFilters); }, 500); return ( diff --git a/src/renderer/features/context-menu/context-menu-items.tsx b/src/renderer/features/context-menu/context-menu-items.tsx index d58aeadcc..b9bb027fe 100644 --- a/src/renderer/features/context-menu/context-menu-items.tsx +++ b/src/renderer/features/context-menu/context-menu-items.tsx @@ -9,6 +9,7 @@ export const QUEUE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { divider: true, id: 'removeFromFavorites' }, { children: true, disabled: false, id: 'setRating' }, { disabled: false, id: 'deselectAll' }, + { divider: true, id: 'showDetails' }, ]; export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ @@ -18,7 +19,9 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { divider: true, id: 'addToPlaylist' }, { id: 'addToFavorites' }, { divider: true, id: 'removeFromFavorites' }, - { children: true, disabled: false, id: 'setRating' }, + { children: true, disabled: false, divider: true, id: 'setRating' }, + { divider: true, id: 'shareItem' }, + { divider: true, id: 'showDetails' }, ]; export const SONG_ALBUM_PAGE: SetContextMenuItems = [ @@ -37,6 +40,7 @@ export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { id: 'addToFavorites' }, { divider: true, id: 'removeFromFavorites' }, { children: true, disabled: false, id: 'setRating' }, + { divider: true, id: 'showDetails' }, ]; export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ @@ -47,6 +51,7 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { id: 'addToFavorites' }, { divider: true, id: 'removeFromFavorites' }, { children: true, disabled: false, id: 'setRating' }, + { divider: true, id: 'showDetails' }, ]; export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ @@ -56,7 +61,9 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { divider: true, id: 'addToPlaylist' }, { id: 'addToFavorites' }, { id: 'removeFromFavorites' }, - { children: true, disabled: false, id: 'setRating' }, + { children: true, disabled: false, divider: true, id: 'setRating' }, + { divider: true, id: 'shareItem' }, + { divider: true, id: 'showDetails' }, ]; export const GENRE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ @@ -74,6 +81,7 @@ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { id: 'addToFavorites' }, { divider: true, id: 'removeFromFavorites' }, { children: true, disabled: false, id: 'setRating' }, + { divider: true, id: 'showDetails' }, ]; export const PLAYLIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 34ebc325e..5bb0a11a6 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -11,6 +11,8 @@ import { import { closeAllModals, openContextModal, openModal } from '@mantine/modals'; import { AnimatePresence } from 'framer-motion'; import isElectron from 'is-electron'; +import { ServerFeature } from '/@/renderer/api/features-types'; +import { hasFeature } from '/@/renderer/api/utils'; import { useTranslation } from 'react-i18next'; import { RiAddBoxFill, @@ -25,6 +27,8 @@ import { RiPlayListAddFill, RiStarFill, RiCloseCircleLine, + RiShareForwardFill, + RiInformationFill, } from 'react-icons/ri'; import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types'; import { @@ -53,6 +57,7 @@ import { } from '/@/renderer/store'; import { usePlaybackType } from '/@/renderer/store/settings.store'; import { Play, PlaybackType } from '/@/renderer/types'; +import { ItemDetailsModal } from '/@/renderer/features/item-details/components/item-details-modal'; type ContextMenuContextProps = { closeContextMenu: () => void; @@ -76,7 +81,7 @@ const ContextMenuContext = createContext({ }, }); -const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating']; +const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating', 'shareItem']; // const NAVIDROME_IGNORED_MENU_ITEMS: ContextMenuItemType[] = []; // const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = []; @@ -600,6 +605,22 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { } }, [ctx.dataNodes, moveToTopOfQueue, playbackType]); + const handleShareItem = useCallback(() => { + if (!ctx.dataNodes && !ctx.data) return; + + const uniqueIds = ctx.data.map((node) => node.id); + + openContextModal({ + innerProps: { + itemIds: uniqueIds, + resourceType: ctx.data[0].itemType, + }, + modal: 'shareItem', + size: 'md', + title: t('page.contextMenu.shareItem', { postProcess: 'sentenceCase' }), + }); + }, [ctx.data, ctx.dataNodes, t]); + const handleRemoveSelected = useCallback(() => { const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId); if (!uniqueIds?.length) return; @@ -627,6 +648,16 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { ctx.tableApi?.deselectAll(); }, [ctx.tableApi]); + const handleOpenItemDetails = useCallback(() => { + const item = ctx.data[0]; + + openModal({ + children: , + size: 'xl', + title: t('page.contextMenu.showDetails', { postProcess: 'titleCase' }), + }); + }, [ctx.data, t]); + const contextMenuItems: Record = useMemo(() => { return { addToFavorites: { @@ -775,20 +806,38 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { onClick: () => {}, rightIcon: , }, + shareItem: { + disabled: !hasFeature(server, ServerFeature.SHARING_ALBUM_SONG), + id: 'shareItem', + label: t('page.contextMenu.shareItem', { postProcess: 'sentenceCase' }), + leftIcon: , + onClick: handleShareItem, + }, + showDetails: { + disabled: ctx.data?.length !== 1 || !ctx.data[0].itemType, + id: 'showDetails', + label: t('page.contextMenu.showDetails', { postProcess: 'sentenceCase' }), + leftIcon: , + onClick: handleOpenItemDetails, + }, }; }, [ + t, handleAddToFavorites, handleAddToPlaylist, + openDeletePlaylistModal, handleDeselectAll, handleMoveToBottom, handleMoveToTop, - handlePlay, handleRemoveFromFavorites, handleRemoveFromPlaylist, handleRemoveSelected, + ctx.data, + handleOpenItemDetails, + handlePlay, handleUpdateRating, - openDeletePlaylistModal, - t, + handleShareItem, + server, ]); const mergedRef = useMergedRef(ref, clickOutsideRef); @@ -819,72 +868,80 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { > {ctx.menuItems?.map((item) => { return ( - - {item.children ? ( - - - - {contextMenuItems[item.id].label} - - - - - {contextMenuItems[ - item.id - ].children?.map((child) => ( - - {child.label} - - ))} - - - - ) : ( - - {contextMenuItems[item.id].label} - - )} - - {item.divider && ( - - )} - + !contextMenuItems[item.id].disabled && ( + + {item.children ? ( + + + + { + contextMenuItems[item.id] + .label + } + + + + + {contextMenuItems[ + item.id + ].children?.map((child) => ( + + {child.label} + + ))} + + + + ) : ( + + {contextMenuItems[item.id].label} + + )} + + {item.divider && ( + + )} + + ) ); })} diff --git a/src/renderer/features/context-menu/events.ts b/src/renderer/features/context-menu/events.ts index dedcf8193..0e7242997 100644 --- a/src/renderer/features/context-menu/events.ts +++ b/src/renderer/features/context-menu/events.ts @@ -28,12 +28,14 @@ export type ContextMenuItemType = | 'addToFavorites' | 'removeFromFavorites' | 'setRating' + | 'shareItem' | 'deletePlaylist' | 'createPlaylist' | 'moveToBottomOfQueue' | 'moveToTopOfQueue' | 'removeFromQueue' - | 'deselectAll'; + | 'deselectAll' + | 'showDetails'; export type SetContextMenuItems = { children?: boolean; diff --git a/src/renderer/features/genres/components/genre-list-grid-view.tsx b/src/renderer/features/genres/components/genre-list-grid-view.tsx index f261f882f..37f92c499 100644 --- a/src/renderer/features/genres/components/genre-list-grid-view.tsx +++ b/src/renderer/features/genres/components/genre-list-grid-view.tsx @@ -13,9 +13,9 @@ import { } from '/@/renderer/components/virtual-grid'; import { useListContext } from '/@/renderer/context/list-context'; import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { CardRow, ListDisplayType } from '/@/renderer/types'; +import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; export const GenreListGridView = ({ gridRef, itemCount }: any) => { const queryClient = useQueryClient(); @@ -24,6 +24,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => { const { pageKey, id } = useListContext(); const { grid, display, filter } = useListStoreByKey({ key: pageKey }); const { setGrid } = useListStoreActions(); + const genrePath = useGenreRoute(); const [searchParams, setSearchParams] = useSearchParams(); const scrollOffset = searchParams.get('scrollOffset'); @@ -137,7 +138,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => { loading={itemCount === undefined || itemCount === null} minimumBatchSize={40} route={{ - route: AppRoute.LIBRARY_GENRES_SONGS, + route: genrePath, slugs: [{ idProperty: 'id', slugProperty: 'genreId' }], }} width={width} 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 5c53bb737..794c8f7cf 100644 --- a/src/renderer/features/genres/components/genre-list-header-filters.tsx +++ b/src/renderer/features/genres/components/genre-list-header-filters.tsx @@ -3,7 +3,14 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { Divider, Flex, Group, Stack } from '@mantine/core'; import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { RiFolder2Fill, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri'; +import { + RiAlbumLine, + RiFolder2Fill, + RiMoreFill, + RiMusic2Line, + RiRefreshLine, + RiSettings3Fill, +} from 'react-icons/ri'; import { queryKeys } from '/@/renderer/api/query-keys'; import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; @@ -15,9 +22,12 @@ import { useContainerQuery } from '/@/renderer/hooks'; import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { GenreListFilter, + GenreTarget, useCurrentServer, + useGeneralSettings, useListStoreActions, useListStoreByKey, + useSettingsStoreActions, } from '/@/renderer/store'; import { ListDisplayType, TableColumn } from '/@/renderer/types'; import i18n from '/@/i18n/i18n'; @@ -52,6 +62,8 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions(); const { display, filter, table, grid } = useListStoreByKey({ key: pageKey }); const cq = useContainerQuery(); + const { genreTarget } = useGeneralSettings(); + const { setGenreBehavior } = useSettingsStoreActions(); const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({ itemType: LibraryItem.GENRE, @@ -208,6 +220,11 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil return filter.musicFolderId !== undefined; }, [filter.musicFolderId]); + const handleGenreToggle = useCallback(() => { + const newState = genreTarget === GenreTarget.ALBUM ? GenreTarget.TRACK : GenreTarget.ALBUM; + setGenreBehavior(newState); + }, [genreTarget, setGenreBehavior]); + return ( + + ; @@ -29,34 +22,18 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade const { t } = useTranslation(); const cq = useContainerQuery(); const server = useCurrentServer(); - const { pageKey } = useListContext(); - const { display, filter } = useListStoreByKey({ key: pageKey }); - const { setFilter, setTablePagination } = useListStoreActions(); - - const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ + const { filter, refresh, search } = useDisplayRefresh({ + gridRef, itemType: LibraryItem.GENRE, server, + tableRef, }); const handleSearch = debounce((e: ChangeEvent) => { - const searchTerm = e.target.value === '' ? undefined : e.target.value; - const updatedFilters = setFilter({ - data: { searchTerm }, - itemType: LibraryItem.GENRE, - key: pageKey, - }) as GenreListFilter; - - const filterWithCustom = { - ...updatedFilters, - }; - - if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { - handleRefreshTable(tableRef, filterWithCustom); - setTablePagination({ data: { currentPage: 0 }, key: pageKey }); - } else { - handleRefreshGrid(gridRef, filterWithCustom); - } + const updatedFilters = search(e) as GenreListFilter; + refresh(updatedFilters); }, 500); + return ( = { + key?: keyof T; + label: string; + postprocess?: string[]; + render?: (item: T) => ReactNode; +}; + +const handleRow = (t: TFunction, item: T, rule: ItemDetailRow) => { + let value: ReactNode; + + if (rule.render) { + value = rule.render(item); + } else { + const prop = item[rule.key!]; + value = prop !== undefined && prop !== null ? String(prop) : null; + } + + if (!value) return null; + + return ( + + {t(rule.label, { postProcess: rule.postprocess || 'sentenceCase' })} + {value} + + ); +}; + +const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) => + (isAlbumArtist ? item.albumArtists : item.artists)?.map((artist, index) => ( + + {index > 0 && } + {artist.id ? ( + + {artist.name || '—'} + + ) : ( + + {artist.name || '-'} + + )} + + )); + +const formatComment = (item: Album | Song) => + item.comment ? {replaceURLWithHTMLLinks(item.comment)} : null; + +const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : ''); + +const FormatGenre = (item: Album | AlbumArtist | Song) => { + const genreRoute = useGenreRoute(); + + return item.genres?.map((genre, index) => ( + + {index > 0 && } + + {genre.name || '—'} + + + )); +}; + +const formatRating = (item: Album | AlbumArtist | Song) => + item.userRating !== null ? ( + + ) : null; + +const BoolField = (key: boolean) => + key ? : ; + +const AlbumPropertyMapping: ItemDetailRow[] = [ + { key: 'name', label: 'common.title' }, + { label: 'entity.albumArtist_one', render: formatArtists(true) }, + { label: 'entity.genre_other', render: FormatGenre }, + { + label: 'common.duration', + render: (album) => album.duration && formatDurationString(album.duration), + }, + { key: 'releaseYear', label: 'filter.releaseYear' }, + { key: 'songCount', label: 'filter.songCount' }, + { label: 'filter.isCompilation', render: (album) => BoolField(album.isCompilation || false) }, + { + key: 'size', + label: 'common.size', + render: (album) => album.size && formatSizeString(album.size), + }, + { + label: 'common.favorite', + render: (album) => BoolField(album.userFavorite), + }, + { label: 'common.rating', render: formatRating }, + { key: 'playCount', label: 'filter.playCount' }, + { + label: 'filter.lastPlayed', + render: (song) => formatDate(song.lastPlayedAt), + }, + { + label: 'common.modified', + render: (song) => formatDate(song.updatedAt), + }, + { label: 'filter.comment', render: formatComment }, + { + label: 'common.mbid', + postprocess: [], + render: (album) => + album.mbzId ? ( + + {album.mbzId} + + ) : null, + }, +]; + +const AlbumArtistPropertyMapping: ItemDetailRow[] = [ + { key: 'name', label: 'common.name' }, + { label: 'entity.genre_other', render: FormatGenre }, + { + label: 'common.duration', + render: (artist) => artist.duration && formatDurationString(artist.duration), + }, + { key: 'songCount', label: 'filter.songCount' }, + { + label: 'common.favorite', + render: (artist) => BoolField(artist.userFavorite), + }, + { label: 'common.rating', render: formatRating }, + { key: 'playCount', label: 'filter.playCount' }, + { + label: 'filter.lastPlayed', + render: (song) => formatDate(song.lastPlayedAt), + }, + { + label: 'common.mbid', + postprocess: [], + render: (artist) => + artist.mbz ? ( + + {artist.mbz} + + ) : null, + }, + { + label: 'common.biography', + render: (artist) => + artist.biography ? ( + + ) : null, + }, +]; + +const SongPropertyMapping: ItemDetailRow[] = [ + { key: 'name', label: 'common.title' }, + { key: 'path', label: 'common.path', render: SongPath }, + { label: 'entity.albumArtist_one', render: formatArtists(true) }, + { key: 'artists', label: 'entity.artist_other', render: formatArtists(false) }, + { + key: 'album', + label: 'entity.album_one', + render: (song) => + song.albumId && + song.album && ( + + {song.album} + + ), + }, + { key: 'discNumber', label: 'common.disc' }, + { key: 'trackNumber', label: 'common.trackNumber' }, + { key: 'releaseYear', label: 'filter.releaseYear' }, + { label: 'entity.genre_other', render: FormatGenre }, + { + label: 'common.duration', + render: (song) => formatDurationString(song.duration), + }, + { 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: 'channels', label: 'common.channel_other' }, + { key: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) }, + { + label: 'common.favorite', + render: (song) => BoolField(song.userFavorite), + }, + { label: 'common.rating', render: formatRating }, + { key: 'playCount', label: 'filter.playCount' }, + { + label: 'filter.lastPlayed', + render: (song) => formatDate(song.lastPlayedAt), + }, + { + label: 'common.modified', + render: (song) => formatDate(song.updatedAt), + }, + { + label: 'common.albumGain', + render: (song) => (song.gain?.album !== undefined ? `${song.gain.album} dB` : null), + }, + { + label: 'common.trackGain', + render: (song) => (song.gain?.track !== undefined ? `${song.gain.track} dB` : null), + }, + { + label: 'common.albumPeak', + render: (song) => (song.peak?.album !== undefined ? `${song.peak.album}` : null), + }, + { + label: 'common.trackPeak', + render: (song) => (song.peak?.track !== undefined ? `${song.peak.track}` : null), + }, + { label: 'filter.comment', render: formatComment }, +]; + +export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => { + const { t } = useTranslation(); + let body: ReactNode; + + switch (item.itemType) { + case LibraryItem.ALBUM: + body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule)); + break; + case LibraryItem.ALBUM_ARTIST: + body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule)); + break; + case LibraryItem.SONG: + body = SongPropertyMapping.map((rule) => handleRow(t, item, rule)); + break; + default: + body = null; + } + + return ( + + + {body} +
+
+ ); +}; diff --git a/src/renderer/features/item-details/components/song-path.tsx b/src/renderer/features/item-details/components/song-path.tsx new file mode 100644 index 000000000..33f663074 --- /dev/null +++ b/src/renderer/features/item-details/components/song-path.tsx @@ -0,0 +1,67 @@ +import { ActionIcon, CopyButton, Group } from '@mantine/core'; +import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; +import { RiCheckFill, RiClipboardFill, RiExternalLinkFill } from 'react-icons/ri'; +import { Tooltip, toast } from '/@/renderer/components'; +import styled from 'styled-components'; + +const util = isElectron() ? window.electron.utils : null; + +export type SongPathProps = { + path: string | null; +}; + +const PathText = styled.div` + user-select: all; +`; + +export const SongPath = ({ path }: SongPathProps) => { + const { t } = useTranslation(); + + if (!path) return null; + + return ( + + + {({ copied, copy }) => ( + + + {copied ? : } + + + )} + + {util && ( + + + { + util.openItem(path).catch((error) => { + toast.error({ + message: (error as Error).message, + title: t('error.openError', { + postProcess: 'sentenceCase', + }), + }); + }); + }} + /> + + + )} + {path} + + ); +}; diff --git a/src/renderer/features/lyrics/queries/lyric-query.ts b/src/renderer/features/lyrics/queries/lyric-query.ts index c19d24384..7d50e821f 100644 --- a/src/renderer/features/lyrics/queries/lyric-query.ts +++ b/src/renderer/features/lyrics/queries/lyric-query.ts @@ -104,7 +104,7 @@ export const useSongLyricsBySong = ( }) .catch(console.error); - if (subsonicLyrics) { + if (subsonicLyrics?.length) { return subsonicLyrics; } } else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) { diff --git a/src/renderer/features/lyrics/synchronized-lyrics.tsx b/src/renderer/features/lyrics/synchronized-lyrics.tsx index 2ec07f37a..19cba8c48 100644 --- a/src/renderer/features/lyrics/synchronized-lyrics.tsx +++ b/src/renderer/features/lyrics/synchronized-lyrics.tsx @@ -41,7 +41,7 @@ const SynchronizedLyricsContainer = styled.div<{ $gap: number }>` transparent 95% ); - @media screen and (width <= 768px) { + @media screen and (orientation: portrait) { padding: 5vh 0; } `; @@ -271,7 +271,12 @@ export const SynchronizedLyrics = ({ return; } - if (!seeked) { + + // If the time goes back to 0 and we are still playing, this suggests that + // we may be playing the same track (repeat one). In this case, we also + // need to restart playback + const restarted = status === PlayerStatus.PLAYING && now === 0; + if (!seeked && !restarted) { return; } diff --git a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx index 8bb6f71ba..251f0196a 100644 --- a/src/renderer/features/lyrics/unsynchronized-lyrics.tsx +++ b/src/renderer/features/lyrics/unsynchronized-lyrics.tsx @@ -34,7 +34,7 @@ const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>` transparent 95% ); - @media screen and (width <= 768px) { + @media screen and (orientation: portrait) { padding: 5vh 0; } `; 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 ea13d0b5d..99c14bf08 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 @@ -14,7 +14,7 @@ import { } from 'react-icons/ri'; import { Song } from '/@/renderer/api/types'; import { usePlayerControls, useQueueControls } from '/@/renderer/store'; -import { PlaybackType, TableType } from '/@/renderer/types'; +import { PlaybackType, PlayerStatus, TableType } from '/@/renderer/types'; import { usePlaybackType } from '/@/renderer/store/settings.store'; import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store'; import { TableConfigDropdown } from '/@/renderer/components/virtual-table'; @@ -91,7 +91,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr mpvPlayer!.pause(); } - remote?.updateSong({ song: undefined }); + remote?.updateSong({ song: undefined, status: PlayerStatus.PAUSED }); setCurrentTime(0); pause(); diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx index 69588792a..de60ba144 100644 --- a/src/renderer/features/now-playing/components/play-queue.tsx +++ b/src/renderer/features/now-playing/components/play-queue.tsx @@ -36,6 +36,7 @@ import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; import { useAppFocus } from '/@/renderer/hooks'; +import { PlayersRef } from '/@/renderer/features/player/ref/players-ref'; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const remote = isElectron() ? window.electron.remote : null; @@ -90,6 +91,15 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.volume(volume); mpvPlayer!.setQueue(playerData, false); + } else { + const player = + playerData.current.player === 1 + ? PlayersRef.current?.player1 + : PlayersRef.current?.player2; + const underlying = player?.getInternalPlayer(); + if (underlying) { + underlying.currentTime = 0; + } } play(); diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index c2d72733f..9c452a67c 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -324,8 +324,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { setSeekValue(e); }} onChangeEnd={(e) => { - handleSeekSlider(e); - setIsSeeking(false); + // There is a timing bug in Mantine in which the onChangeEnd + // event fires before onChange. Add a small delay to force + // onChangeEnd to happen after onCHange + setTimeout(() => { + handleSeekSlider(e); + setIsSeeking(false); + }, 50); }} /> 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 20ae09757..269517dea 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -206,10 +206,7 @@ export const FullScreenPlayerImage = () => { justify="flex-start" p="1rem" > - + { srcLoaded: true, }); - const imageUrl = currentSong?.imageUrl; + const imageUrl = currentSong?.imageUrl && currentSong.imageUrl.replace(/size=\d+/g, 'size=500'); const backgroundImage = imageUrl && dynamicIsImage - ? `url("${imageUrl - .replace(/size=\d+/g, 'size=500') - .replace(currentSong.id, currentSong.albumId)}` + ? `url("${imageUrl.replace(currentSong.id, currentSong.albumId)}"), url("${imageUrl}")` : mainBackground; return ( diff --git a/src/renderer/features/player/components/left-controls.tsx b/src/renderer/features/player/components/left-controls.tsx index 928f005e1..70076b285 100644 --- a/src/renderer/features/player/components/left-controls.tsx +++ b/src/renderer/features/player/components/left-controls.tsx @@ -20,6 +20,7 @@ import { fadeIn } from '/@/renderer/styles'; import { LibraryItem } from '/@/renderer/api/types'; import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu'; +import { Separator } from '/@/renderer/components/separator'; const ImageWrapper = styled.div` position: relative; @@ -236,16 +237,7 @@ export const LeftControls = () => { {artists?.map((artist, index) => ( - {index > 0 && ( - - , - - )}{' '} + {index > 0 && } { const handleRepeatAll = { local: () => { const playerData = next(); - mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); + mprisUpdateSong({ song: playerData.current.song }); mpvPlayer!.setQueue(playerData); - mpvPlayer!.next(); }, web: () => { const playerData = next(); - mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); + mprisUpdateSong({ song: playerData.current.song }); }, }; @@ -324,17 +323,12 @@ export const useCenterControls = (args: { playersRef: any }) => { if (isLastTrack) { const playerData = setCurrentIndex(0); mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED }); - mpvPlayer!.setQueue(playerData); - mpvPlayer!.pause(); + mpvPlayer!.setQueue(playerData, true); pause(); } else { const playerData = next(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); mpvPlayer!.setQueue(playerData); - mpvPlayer!.next(); } }, web: () => { @@ -342,16 +336,13 @@ export const useCenterControls = (args: { playersRef: any }) => { const playerData = setCurrentIndex(0); mprisUpdateSong({ song: playerData.current.song, - status: PlayerStatus.PLAYING, + status: PlayerStatus.PAUSED, }); resetPlayers(); pause(); } else { const playerData = next(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); resetPlayers(); } }, @@ -359,18 +350,16 @@ export const useCenterControls = (args: { playersRef: any }) => { const handleRepeatOne = { local: () => { - const playerData = next(); - mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); - mpvPlayer!.setQueue(playerData); - mpvPlayer!.next(); + if (!isLastTrack) { + const playerData = next(); + mprisUpdateSong({ song: playerData.current.song }); + mpvPlayer!.setQueue(playerData); + } }, web: () => { if (!isLastTrack) { const playerData = next(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); } }, }; @@ -424,36 +413,22 @@ export const useCenterControls = (args: { playersRef: any }) => { local: () => { if (!isFirstTrack) { const playerData = previous(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); mpvPlayer!.setQueue(playerData); - mpvPlayer!.previous(); } else { const playerData = setCurrentIndex(queue.length - 1); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); mpvPlayer!.setQueue(playerData); - mpvPlayer!.previous(); } }, web: () => { if (isFirstTrack) { const playerData = setCurrentIndex(queue.length - 1); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); resetPlayers(); } else { const playerData = previous(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); resetPlayers(); } }, @@ -461,13 +436,19 @@ export const useCenterControls = (args: { playersRef: any }) => { const handleRepeatNone = { local: () => { - const playerData = previous(); - remote?.updateSong({ - currentTime: usePlayerStore.getState().current.time, - song: playerData.current.song, - }); - mpvPlayer!.setQueue(playerData); - mpvPlayer!.previous(); + if (isFirstTrack) { + const playerData = setCurrentIndex(0); + mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED }); + mpvPlayer!.setQueue(playerData, true); + pause(); + } else { + const playerData = previous(); + mprisUpdateSong({ + currentTime: usePlayerStore.getState().current.time, + song: playerData.current.song, + }); + mpvPlayer!.setQueue(playerData); + } }, web: () => { if (isFirstTrack) { @@ -476,10 +457,7 @@ export const useCenterControls = (args: { playersRef: any }) => { pause(); } else { const playerData = previous(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); + mprisUpdateSong({ song: playerData.current.song }); resetPlayers(); } }, @@ -487,21 +465,13 @@ export const useCenterControls = (args: { playersRef: any }) => { const handleRepeatOne = { local: () => { - if (!isFirstTrack) { - const playerData = previous(); - mprisUpdateSong({ - song: playerData.current.song, - status: PlayerStatus.PLAYING, - }); - mpvPlayer!.setQueue(playerData); - mpvPlayer!.previous(); - } else { - mpvPlayer!.stop(); - } + const playerData = previous(); + mprisUpdateSong({ song: playerData.current.song }); + mpvPlayer!.setQueue(playerData); }, web: () => { const playerData = previous(); - mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); + mprisUpdateSong({ song: playerData.current.song }); resetPlayers(); }, }; @@ -538,7 +508,7 @@ export const useCenterControls = (args: { playersRef: any }) => { ]); const handlePlayPause = useCallback(() => { - if (queue) { + if (queue.length > 0) { if (playerStatus === PlayerStatus.PAUSED) { return handlePlay(); } diff --git a/src/renderer/features/player/hooks/use-scrobble.ts b/src/renderer/features/player/hooks/use-scrobble.ts index b2a7b9a32..a93dad13c 100644 --- a/src/renderer/features/player/hooks/use-scrobble.ts +++ b/src/renderer/features/player/hooks/use-scrobble.ts @@ -1,7 +1,7 @@ import { useEffect, useCallback, useState, useRef } from 'react'; import { QueueSong, ServerType } from '/@/renderer/api/types'; import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation'; -import { useCurrentStatus, usePlayerStore } from '/@/renderer/store'; +import { usePlayerStore } from '/@/renderer/store'; import { usePlaybackSettings } from '/@/renderer/store/settings.store'; import { PlayerStatus } from '/@/renderer/types'; @@ -52,7 +52,6 @@ const checkScrobbleConditions = (args: { }; export const useScrobble = () => { - const status = useCurrentStatus(); const scrobbleSettings = usePlaybackSettings().scrobble; const isScrobbleEnabled = scrobbleSettings?.enabled; const sendScrobble = useSendScrobble(); @@ -94,6 +93,7 @@ export const useScrobble = () => { if (progressIntervalId.current) { clearInterval(progressIntervalId.current); + progressIntervalId.current = null; } // const currentSong = current[0] as QueueSong | undefined; @@ -135,9 +135,13 @@ export const useScrobble = () => { clearTimeout(songChangeTimeoutId.current as ReturnType); songChangeTimeoutId.current = setTimeout(() => { const currentSong = current[0] as QueueSong | undefined; + // Get the current status from the state, not variable. This is because + // of a timing issue where, when playing the first track, the first + // event is song, and then the event is play + const currentStatus = usePlayerStore.getState().current.status; // Send start scrobble when song changes and the new song is playing - if (status === PlayerStatus.PLAYING && currentSong?.id) { + if (currentStatus === PlayerStatus.PLAYING && currentSong?.id) { sendScrobble.mutate({ query: { event: 'start', @@ -149,6 +153,12 @@ export const useScrobble = () => { }); if (currentSong?.serverType === ServerType.JELLYFIN) { + // It is possible that another function sets an interval. + // We only want one running, so clear the existing interval + if (progressIntervalId.current) { + clearInterval(progressIntervalId.current); + } + progressIntervalId.current = setInterval(() => { const currentTime = usePlayerStore.getState().current.time; handleScrobbleFromSeek(currentTime); @@ -163,7 +173,6 @@ export const useScrobble = () => { scrobbleSettings?.scrobbleAtPercentage, isCurrentSongScrobbled, sendScrobble, - status, handleScrobbleFromSeek, ], ); @@ -200,8 +209,14 @@ export const useScrobble = () => { }); if (currentSong?.serverType === ServerType.JELLYFIN) { + // It is possible that another function sets an interval. + // We only want one running, so clear the existing interval + if (progressIntervalId.current) { + clearInterval(progressIntervalId.current); + } + progressIntervalId.current = setInterval(() => { - const currentTime = currentTimeSec; + const currentTime = usePlayerStore.getState().current.time; handleScrobbleFromSeek(currentTime); }, 10000); } @@ -220,6 +235,7 @@ export const useScrobble = () => { if (progressIntervalId.current) { clearInterval(progressIntervalId.current as ReturnType); + progressIntervalId.current = null; } } else { const isLastTrackInQueue = usePlayerStore.getState().actions.checkIsLastTrack(); diff --git a/src/renderer/features/playlists/components/playlist-list-grid-view.tsx b/src/renderer/features/playlists/components/playlist-list-grid-view.tsx index 14abc68e8..cd8ee5b14 100644 --- a/src/renderer/features/playlists/components/playlist-list-grid-view.tsx +++ b/src/renderer/features/playlists/components/playlist-list-grid-view.tsx @@ -20,10 +20,10 @@ import { VirtualInfiniteGridRef, } from '/@/renderer/components/virtual-grid'; import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useGeneralSettings, useListStoreByKey } from '/@/renderer/store'; import { CardRow, ListDisplayType } from '/@/renderer/types'; +import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite'; interface PlaylistListGridViewProps { gridRef: MutableRefObject; @@ -38,34 +38,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie const { display, grid, filter } = useListStoreByKey({ key: pageKey }); const { setGrid } = useListStoreActions(); const { defaultFullPlaylist } = useGeneralSettings(); - - const createFavoriteMutation = useCreateFavorite({}); - const deleteFavoriteMutation = useDeleteFavorite({}); - - const handleFavorite = (options: { - id: string[]; - isFavorite: boolean; - itemType: LibraryItem; - }) => { - const { id, itemType, isFavorite } = options; - if (isFavorite) { - deleteFavoriteMutation.mutate({ - query: { - id, - type: itemType, - }, - serverId: server?.id, - }); - } else { - createFavoriteMutation.mutate({ - query: { - id, - type: itemType, - }, - serverId: server?.id, - }); - } - }; + const handleFavorite = useHandleFavorite({ gridRef, server }); const cardRows = useMemo(() => { const rows: CardRow[] = defaultFullPlaylist diff --git a/src/renderer/features/playlists/components/playlist-list-header.tsx b/src/renderer/features/playlists/components/playlist-list-header.tsx index ec4f01389..faf1974ee 100644 --- a/src/renderer/features/playlists/components/playlist-list-header.tsx +++ b/src/renderer/features/playlists/components/playlist-list-header.tsx @@ -8,15 +8,12 @@ import { CreatePlaylistForm } from '/@/renderer/features/playlists/components/cr import { PlaylistListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-list-header-filters'; import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; -import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store'; -import { ListDisplayType } from '/@/renderer/types'; +import { PlaylistListFilter, useCurrentServer } from '/@/renderer/store'; import debounce from 'lodash/debounce'; import { useTranslation } from 'react-i18next'; import { RiFileAddFill } from 'react-icons/ri'; import { LibraryItem, ServerType } from '/@/renderer/api/types'; -import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh'; -import { useListContext } from '/@/renderer/context/list-context'; -import { useListStoreByKey } from '../../../store/list.store'; +import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh'; interface PlaylistListHeaderProps { gridRef: MutableRefObject; @@ -26,11 +23,8 @@ interface PlaylistListHeaderProps { export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistListHeaderProps) => { const { t } = useTranslation(); - const { pageKey } = useListContext(); const cq = useContainerQuery(); const server = useCurrentServer(); - const { setFilter, setTablePagination } = useListStoreActions(); - const { display, filter } = useListStoreByKey({ key: pageKey }); const handleCreatePlaylistModal = () => { openModal({ @@ -43,25 +37,16 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis }); }; - const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ + const { filter, refresh, search } = useDisplayRefresh({ + gridRef, itemType: LibraryItem.PLAYLIST, server, + tableRef, }); const handleSearch = debounce((e: ChangeEvent) => { - const searchTerm = e.target.value === '' ? undefined : e.target.value; - const updatedFilters = setFilter({ - data: { searchTerm }, - itemType: LibraryItem.PLAYLIST, - key: pageKey, - }) as PlaylistListFilter; - - if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { - handleRefreshTable(tableRef, updatedFilters); - setTablePagination({ data: { currentPage: 0 }, key: pageKey }); - } else { - handleRefreshGrid(gridRef, updatedFilters); - } + const updatedFilters = search(e) as PlaylistListFilter; + refresh(updatedFilters); }, 500); return ( diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index 1192fdad1..72e1d8761 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -76,7 +76,7 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl }); const isPublicDisplayed = server?.type === ServerType.NAVIDROME; - const isOwnerDisplayed = server?.type === ServerType.NAVIDROME; + const isOwnerDisplayed = server?.type === ServerType.NAVIDROME && userList; const isSubmitDisabled = !form.values.name || mutation.isLoading; return ( @@ -154,11 +154,17 @@ export const openUpdatePlaylistModal = async (args: { const users = server?.type === ServerType.NAVIDROME - ? await queryClient.fetchQuery({ - queryFn: ({ signal }) => - api.controller.getUserList({ apiClientProps: { server, signal }, query }), - queryKey: queryKeys.users.list(server?.id || '', query), - }) + ? await queryClient + .fetchQuery({ + queryFn: ({ signal }) => + api.controller.getUserList({ apiClientProps: { server, signal }, query }), + queryKey: queryKeys.users.list(server?.id || '', query), + }) + .catch((error) => { + // This eror most likely happens if the user is not an admin + console.error(error); + return null; + }) : null; openModal({ diff --git a/src/renderer/features/search/components/search-content.tsx b/src/renderer/features/search/components/search-content.tsx index 2ee955aa8..38ffc5c9e 100644 --- a/src/renderer/features/search/components/search-content.tsx +++ b/src/renderer/features/search/components/search-content.tsx @@ -15,7 +15,7 @@ import { SONG_CONTEXT_MENU_ITEMS, } from '/@/renderer/features/context-menu/context-menu-items'; import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { useCurrentServer, usePlayButtonBehavior } from '/@/renderer/store'; +import { useCurrentServer, useListStoreByKey, usePlayButtonBehavior } from '/@/renderer/store'; interface SearchContentProps { tableRef: MutableRefObject; @@ -27,6 +27,10 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => { const { itemType } = useParams() as { itemType: LibraryItem }; const [searchParams] = useSearchParams(); const pageKey = itemType; + const { filter } = useListStoreByKey({ + filter: { searchTerm: searchParams.get('query') || '' }, + key: itemType, + }); const handlePlayQueueAdd = usePlayQueueAdd(); const playButtonBehavior = usePlayButtonBehavior(); @@ -59,22 +63,26 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => { break; case LibraryItem.SONG: handlePlayQueueAdd?.({ - byData: [e.data], + byItemType: { + id: [], + type: LibraryItem.SONG, + }, + initialSongId: e.data.id, playType: playButtonBehavior, + query: { + startIndex: 0, + ...filter, + }, }); break; } }; - const customFilters = { - searchTerm: searchParams.get('query') || '', - }; - const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); const tableProps = useVirtualTable({ contextMenu: contextMenuItems(), - customFilters, + customFilters: filter, itemType, pageKey, server, diff --git a/src/renderer/features/search/components/search-header.tsx b/src/renderer/features/search/components/search-header.tsx index 0e31a6dac..16fc2f8bd 100644 --- a/src/renderer/features/search/components/search-header.tsx +++ b/src/renderer/features/search/components/search-header.tsx @@ -11,6 +11,7 @@ import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { AppRoute } from '/@/renderer/router/routes'; +import { useListStoreByKey } from '/@/renderer/store'; interface SearchHeaderProps { navigationId: string; @@ -23,6 +24,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => { const [searchParams, setSearchParams] = useSearchParams(); const cq = useContainerQuery(); const server = useCurrentServer(); + const { filter } = useListStoreByKey({ key: itemType }); const { handleRefreshTable } = useListFilterRefresh({ itemType, @@ -30,9 +32,8 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => { }); const handleSearch = debounce((e: ChangeEvent) => { - if (!e.target.value) return; setSearchParams({ query: e.target.value }, { replace: true, state: { navigationId } }); - handleRefreshTable(tableRef, { searchTerm: e.target.value }); + handleRefreshTable(tableRef, { ...filter, searchTerm: e.target.value }); }, 200); return ( diff --git a/src/renderer/features/servers/components/server-list.tsx b/src/renderer/features/servers/components/server-list.tsx index 05ecaa95f..256b4e415 100644 --- a/src/renderer/features/servers/components/server-list.tsx +++ b/src/renderer/features/servers/components/server-list.tsx @@ -1,6 +1,6 @@ import { ChangeEvent } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; -import { Accordion, Button, ContextModalVars, Switch } from '/@/renderer/components'; +import { Accordion, Button, ContextModalVars, Switch, Text } from '/@/renderer/components'; import { useLocalStorage } from '@mantine/hooks'; import { openContextModal } from '@mantine/modals'; import isElectron from 'is-electron'; @@ -8,13 +8,14 @@ import { useTranslation } from 'react-i18next'; import { RiAddFill, RiServerFill } from 'react-icons/ri'; import { AddServerForm } from '/@/renderer/features/servers/components/add-server-form'; import { ServerListItem } from '/@/renderer/features/servers/components/server-list-item'; -import { useServerList } from '/@/renderer/store'; +import { useCurrentServer, useServerList } from '/@/renderer/store'; import { titleCase } from '/@/renderer/utils'; const localSettings = isElectron() ? window.electron.localSettings : null; export const ServerList = () => { const { t } = useTranslation(); + const currentServer = useCurrentServer(); const serverListQuery = useServerList(); const handleAddServerModal = () => { @@ -90,7 +91,9 @@ export const ServerList = () => { > }> - {titleCase(server?.type)} - {server?.name} + + {titleCase(server?.type)} - {server?.name} + diff --git a/src/renderer/features/settings/components/general/control-settings.tsx b/src/renderer/features/settings/components/general/control-settings.tsx index fb882d2a7..57232129d 100644 --- a/src/renderer/features/settings/components/general/control-settings.tsx +++ b/src/renderer/features/settings/components/general/control-settings.tsx @@ -4,6 +4,7 @@ import isElectron from 'is-electron'; import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components'; import { SettingsSection } from '/@/renderer/features/settings/components/settings-section'; import { + GenreTarget, SideQueueType, useGeneralSettings, useSettingsStoreActions, @@ -341,6 +342,41 @@ export const ControlSettings = () => { }), title: t('setting.externalLinks', { postProcess: 'sentenceCase' }), }, + { + control: ( +