mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-12 15:23:01 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5900d41e0a | |||
| efe94b3a3b | |||
| 231b6f3865 | |||
| 2fbd3ab02d |
@@ -169,10 +169,6 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi
|
|||||||
- [Qm-Music](https://github.com/chenqimiao/qm-music)
|
- [Qm-Music](https://github.com/chenqimiao/qm-music)
|
||||||
- More (?)
|
- More (?)
|
||||||
|
|
||||||
- [Plex](https://www.plex.tv/media-server-downloads)
|
|
||||||
- [Feishin fork by lux032](https://github.com/lux032/feishin) - Plex is not natively supported. Use the fork by lux032 to use Plex with Feishin.
|
|
||||||
|
|
||||||
|
|
||||||
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -43,11 +43,9 @@ mac:
|
|||||||
icon: assets/icons/icon.icns
|
icon: assets/icons/icon.icns
|
||||||
type: distribution
|
type: distribution
|
||||||
hardenedRuntime: false
|
hardenedRuntime: false
|
||||||
identity: '-'
|
identity: "-"
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: false
|
notarize: false
|
||||||
extendInfo:
|
|
||||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
|
||||||
@@ -62,7 +60,7 @@ linux:
|
|||||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
appimage: '1.0.2'
|
appimage: "1.0.2"
|
||||||
|
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
afterAllArtifactBuild: scripts/after-all-artifact-build.mjs
|
afterAllArtifactBuild: scripts/after-all-artifact-build.mjs
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import conditionalImportPlugin from 'vite-plugin-conditional-import';
|
import conditionalImportPlugin from 'vite-plugin-conditional-import';
|
||||||
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
|
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
|
||||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||||
|
|
||||||
import { createReactPlugin } from './vite.react-plugin';
|
|
||||||
|
|
||||||
const currentOSEnv = process.platform;
|
const currentOSEnv = process.platform;
|
||||||
const electronRendererTarget = 'chrome87';
|
const electronRendererTarget = 'chrome87';
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ const config: UserConfig = {
|
|||||||
localsConvention: 'camelCase',
|
localsConvention: 'camelCase',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [createReactPlugin(), ViteEjsPlugin({ web: false })],
|
plugins: [react(), ViteEjsPlugin({ web: false })],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'/@/i18n': resolve('src/i18n'),
|
'/@/i18n': resolve('src/i18n'),
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ export default tseslint.config(
|
|||||||
'react-refresh': eslintPluginReactRefresh,
|
'react-refresh': eslintPluginReactRefresh,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...eslintPluginReactHooks.configs['recommended-latest'].rules,
|
...eslintPluginReactHooks.configs.recommended.rules,
|
||||||
...eslintPluginReactRefresh.configs.vite.rules,
|
...eslintPluginReactRefresh.configs.vite.rules,
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||||
|
|||||||
+74
-78
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.10.0",
|
"version": "1.9.0",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
@@ -31,36 +31,36 @@
|
|||||||
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"lint": "pnpm run typecheck && pnpm run lint-code && pnpm run lint-styles",
|
"lint": "pnpm run typecheck && pnpm run lint-code && pnpm run lint-styles",
|
||||||
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
|
|
||||||
"lint-code": "eslint --max-warnings=0 --cache .",
|
"lint-code": "eslint --max-warnings=0 --cache .",
|
||||||
"lint-code:fix": "eslint --cache --fix .",
|
"lint-code:fix": "eslint --cache --fix .",
|
||||||
"lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
|
"lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
|
||||||
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
|
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
|
||||||
|
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
|
||||||
"package": "pnpm run build && electron-builder",
|
"package": "pnpm run build && electron-builder",
|
||||||
"package:dev": "pnpm run build && electron-builder --dir",
|
"package:dev": "pnpm run build && electron-builder --dir",
|
||||||
"package:linux": "pnpm run build && electron-builder --linux",
|
"package:linux": "pnpm run build && electron-builder --linux",
|
||||||
"package:linux:pr": "pnpm run build && electron-builder --linux --publish never",
|
|
||||||
"package:linux-arm64:pr": "pnpm run build && electron-builder --linux --arm64 --publish never",
|
"package:linux-arm64:pr": "pnpm run build && electron-builder --linux --arm64 --publish never",
|
||||||
|
"package:linux:pr": "pnpm run build && electron-builder --linux --publish never",
|
||||||
"package:mac": "pnpm run build && electron-builder --mac",
|
"package:mac": "pnpm run build && electron-builder --mac",
|
||||||
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
|
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
|
||||||
"package:win": "pnpm run build && electron-builder --win",
|
"package:win": "pnpm run build && electron-builder --win",
|
||||||
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
|
|
||||||
"package:win-arm64:pr": "pnpm run build && electron-builder --win --arm64 --publish never",
|
"package:win-arm64:pr": "pnpm run build && electron-builder --win --arm64 --publish never",
|
||||||
|
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
|
||||||
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
|
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
|
||||||
"publish:linux:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux",
|
|
||||||
"publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
|
|
||||||
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
|
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
|
||||||
"publish:linux-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux --arm64",
|
"publish:linux-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux --arm64",
|
||||||
"publish:linux-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux --arm64",
|
"publish:linux-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux --arm64",
|
||||||
|
"publish:linux:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux",
|
||||||
|
"publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
|
||||||
"publish:mac": "pnpm run build && electron-builder --publish always --mac",
|
"publish:mac": "pnpm run build && electron-builder --publish always --mac",
|
||||||
"publish:mac:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --mac",
|
"publish:mac:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --mac",
|
||||||
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
|
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
|
||||||
"publish:win": "pnpm run build && electron-builder --publish always --win",
|
"publish:win": "pnpm run build && electron-builder --publish always --win",
|
||||||
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
|
|
||||||
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
|
|
||||||
"publish:win-arm64": "pnpm run build && electron-builder --publish always --win --arm64",
|
"publish:win-arm64": "pnpm run build && electron-builder --publish always --win --arm64",
|
||||||
"publish:win-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win --arm64",
|
"publish:win-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win --arm64",
|
||||||
"publish:win-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win --arm64",
|
"publish:win-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win --arm64",
|
||||||
|
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
|
||||||
|
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
@@ -68,123 +68,119 @@
|
|||||||
"version": "pnpm version --no-git-tag-version",
|
"version": "pnpm version --no-git-tag-version",
|
||||||
"postversion": "node ./scripts/update-app-stream.mjs"
|
"postversion": "node ./scripts/update-app-stream.mjs"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"react-router": "7.14.0",
|
|
||||||
"xml2js": "0.5.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.5",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@mantine/colors-generator": "^8.3.18",
|
"@mantine/colors-generator": "^8.3.8",
|
||||||
"@mantine/core": "^8.3.18",
|
"@mantine/core": "^8.3.8",
|
||||||
"@mantine/dates": "^8.3.18",
|
"@mantine/dates": "^8.3.8",
|
||||||
"@mantine/form": "^8.3.18",
|
"@mantine/form": "^8.3.8",
|
||||||
"@mantine/hooks": "^8.3.18",
|
"@mantine/hooks": "^8.3.8",
|
||||||
"@mantine/modals": "^8.3.18",
|
"@mantine/modals": "^8.3.8",
|
||||||
"@mantine/notifications": "^8.3.18",
|
"@mantine/notifications": "^8.3.8",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@tanstack/react-query": "^5.96.2",
|
"@tanstack/react-query": "^5.90.9",
|
||||||
"@tanstack/react-query-devtools": "^5.96.2",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@tanstack/react-query-persist-client": "^5.96.2",
|
"@tanstack/react-query-persist-client": "^5.90.11",
|
||||||
"@ts-rest/core": "^3.52.1",
|
"@ts-rest/core": "^3.52.1",
|
||||||
"@wavesurfer/react": "^1.0.12",
|
"@wavesurfer/react": "^1.0.11",
|
||||||
"@xhayper/discord-rpc": "^1.3.3",
|
"@xhayper/discord-rpc": "^1.3.0",
|
||||||
"audiomotion-analyzer": "^4.5.4",
|
"audiomotion-analyzer": "^4.5.1",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.13.5",
|
||||||
"butterchurn": "3.0.0-beta.5",
|
"butterchurn": "^3.0.0-beta.5",
|
||||||
"butterchurn-presets": "3.0.0-beta.4",
|
"butterchurn-presets": "^3.0.0-beta.4",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.19",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.0",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.8.3",
|
"electron-updater": "^6.6.2",
|
||||||
"fast-average-color": "9.5.0",
|
"fast-average-color": "^9.5.0",
|
||||||
"fast-xml-parser": "^5.5.10",
|
"fast-xml-parser": "^5.3.8",
|
||||||
"format-duration": "^3.0.2",
|
"format-duration": "^3.0.2",
|
||||||
"fuse.js": "^7.2.0",
|
"fuse.js": "^7.1.0",
|
||||||
"i18next": "^25.10.10",
|
"i18next": "^25.6.2",
|
||||||
"icecast-metadata-stats": "^0.1.12",
|
"icecast-metadata-stats": "^0.1.12",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"immer": "^10.2.0",
|
"immer": "^10.2.0",
|
||||||
"is-electron": "^2.2.2",
|
"is-electron": "^2.2.2",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.17.23",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.23.24",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||||
"overlayscrollbars": "^2.14.0",
|
"nuqs": "^2.7.1",
|
||||||
|
"overlayscrollbars": "^2.11.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"qs": "^6.15.0",
|
"qs": "^6.14.2",
|
||||||
"react": "^19.2.4",
|
"react": "^19.1.0",
|
||||||
"react-call": "^1.8.2",
|
"react-call": "^1.8.1",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.1.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^5.0.0",
|
||||||
"react-i18next": "^16.6.6",
|
"react-i18next": "^16.3.3",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-player": "^2.16.1",
|
"react-player": "^2.16.0",
|
||||||
"react-router": "^7.14.0",
|
"react-router": "^7.13.1",
|
||||||
"react-split-pane": "^3.2.0",
|
"react-split-pane": "^3.0.4",
|
||||||
"react-virtualized-auto-sizer": "^1.0.26",
|
"react-virtualized-auto-sizer": "^1.0.26",
|
||||||
"react-window": "1.8.11",
|
"react-window": "1.8.11",
|
||||||
"react-window-v2": "npm:react-window@^2.2.7",
|
"react-window-v2": "npm:react-window@^2.2.3",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.5.4",
|
||||||
"string-to-color": "^2.2.2",
|
"string-to-color": "^2.2.2",
|
||||||
"wavesurfer.js": "^7.12.5",
|
"wavesurfer.js": "^7.11.1",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.18.2",
|
||||||
"zod": "^3.25.76",
|
"zod": "^3.22.3",
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||||
"@types/electron-localshortcut": "^3.1.3",
|
"@types/electron-localshortcut": "^3.1.0",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.18",
|
||||||
"@types/md5": "^2.3.6",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react": "^5.2.0",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"electron": "^39.8.6",
|
"electron": "^39.4.0",
|
||||||
"electron-builder": "^26.8.2",
|
"electron-builder": "^26.8.2",
|
||||||
"electron-devtools-installer": "^4.0.0",
|
"electron-devtools-installer": "^4.0.0",
|
||||||
"electron-vite": "^4.0.1",
|
"electron-vite": "^4.0.1",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-perfectionist": "^4.15.1",
|
"eslint-plugin-perfectionist": "^4.13.0",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"eslint-plugin-prettier": "^5.4.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"i18next-parser": "^9.4.0",
|
"i18next-parser": "^9.3.0",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-packagejson": "^2.5.22",
|
"prettier-plugin-packagejson": "^2.5.19",
|
||||||
"stylelint": "^16.26.1",
|
"stylelint": "^16.25.0",
|
||||||
"stylelint-config-css-modules": "^4.6.0",
|
"stylelint-config-css-modules": "^4.5.1",
|
||||||
"stylelint-config-recess-order": "^7.7.0",
|
"stylelint-config-recess-order": "^7.4.0",
|
||||||
"stylelint-config-standard": "^39.0.1",
|
"stylelint-config-standard": "^39.0.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.2.2",
|
||||||
"vite-plugin-conditional-import": "^0.1.7",
|
"vite-plugin-conditional-import": "^0.1.7",
|
||||||
"vite-plugin-dynamic-import": "^1.6.0",
|
"vite-plugin-dynamic-import": "^1.6.0",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"vite-plugin-pwa": "^1.2.0"
|
"vite-plugin-pwa": "^1.1.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
Generated
+2400
-2432
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { defineConfig, normalizePath } from 'vite';
|
import { defineConfig, normalizePath } from 'vite';
|
||||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||||
|
|
||||||
import { version } from './package.json';
|
import { version } from './package.json';
|
||||||
import { createReactPlugin } from './vite.react-plugin';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
@@ -35,7 +35,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
createReactPlugin(),
|
react(),
|
||||||
ViteEjsPlugin({
|
ViteEjsPlugin({
|
||||||
prod: process.env.NODE_ENV === 'production',
|
prod: process.env.NODE_ENV === 'production',
|
||||||
root: normalizePath(path.resolve(__dirname, './src/remote')),
|
root: normalizePath(path.resolve(__dirname, './src/remote')),
|
||||||
|
|||||||
@@ -1214,12 +1214,6 @@
|
|||||||
"mainText": "drop a file here"
|
"mainText": "drop a file here"
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
"systemAudioConsentAllow": "Allow",
|
|
||||||
"systemAudioConsentBody": "The visualizer requires access to the system audio to work",
|
|
||||||
"systemAudioConsentDecline": "Deny",
|
|
||||||
"systemAudioConsentTitle": "Allow access to system audio?",
|
|
||||||
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
|
|
||||||
"systemAudioNoAudioTrack": "No audio track was returned. Ensure audio capture is enabled when prompted.",
|
|
||||||
"visualizerType": "Visualizer Type",
|
"visualizerType": "Visualizer Type",
|
||||||
"cyclePresets": "Cycle Presets",
|
"cyclePresets": "Cycle Presets",
|
||||||
"cycleTime": "Cycle Time (seconds)",
|
"cycleTime": "Cycle Time (seconds)",
|
||||||
|
|||||||
@@ -1335,8 +1335,6 @@
|
|||||||
"d": "D",
|
"d": "D",
|
||||||
"z": "Z"
|
"z": "Z"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"systemAudioCaptureFailed": "無法開始擷取:{{message}}",
|
|
||||||
"systemAudioNoAudioTrack": "沒有回傳任何音軌。確保在提示時啟用音訊擷取。"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export const store = new Store<any>({
|
|||||||
playbackType: 'web',
|
playbackType: 'web',
|
||||||
should_prompt_accessibility: true,
|
should_prompt_accessibility: true,
|
||||||
shown_accessibility_warning: false,
|
shown_accessibility_warning: false,
|
||||||
visualizer_system_audio_consent_granted: false,
|
|
||||||
window_enable_tray: true,
|
window_enable_tray: true,
|
||||||
window_exit_to_tray: false,
|
window_exit_to_tray: false,
|
||||||
window_minimize_to_tray: false,
|
window_minimize_to_tray: false,
|
||||||
|
|||||||
@@ -150,23 +150,6 @@ ipcMain.on(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the served id is an empty string, this is a radio
|
|
||||||
// Use a limited subset of the fields
|
|
||||||
if (song._serverId === '') {
|
|
||||||
// The id as passed in from use-mpris is radio- plus the radio ID
|
|
||||||
// If there are spaces or some other characters, this causes MPRIS to error and
|
|
||||||
// disconnect the bus. To prevent this, just use a fake track/radio
|
|
||||||
mprisPlayer.metadata = {
|
|
||||||
'mpris:trackid': mprisPlayer.objectPath(`track/radio`),
|
|
||||||
'xesam:album': song.album || null,
|
|
||||||
'xesam:artist': song.artists?.length
|
|
||||||
? song.artists.map((artist) => artist.name)
|
|
||||||
: null,
|
|
||||||
'xesam:title': song.name || null,
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mprisPlayer.metadata = {
|
mprisPlayer.metadata = {
|
||||||
'mpris:artUrl': imageUrl || null,
|
'mpris:artUrl': imageUrl || null,
|
||||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e3) : null,
|
||||||
|
|||||||
+8
-116
@@ -5,7 +5,6 @@ import {
|
|||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
BrowserWindowConstructorOptions,
|
BrowserWindowConstructorOptions,
|
||||||
desktopCapturer,
|
|
||||||
globalShortcut,
|
globalShortcut,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -30,7 +29,7 @@ import packageJson from '../../package.json';
|
|||||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||||
import { shutdownServer } from './features/core/remote';
|
import { shutdownServer } from './features/core/remote';
|
||||||
import { store } from './features/core/settings';
|
import { store } from './features/core/settings';
|
||||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
import MenuBuilder from './menu';
|
||||||
import {
|
import {
|
||||||
autoUpdaterLogInterface,
|
autoUpdaterLogInterface,
|
||||||
createLog,
|
createLog,
|
||||||
@@ -42,7 +41,7 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
import './features';
|
import './features';
|
||||||
|
|
||||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||||
|
|
||||||
const ALPHA_UPDATER_CONFIG: {
|
const ALPHA_UPDATER_CONFIG: {
|
||||||
bucket: string;
|
bucket: string;
|
||||||
@@ -278,13 +277,6 @@ let tray: null | Tray = null;
|
|||||||
let exitFromTray = false;
|
let exitFromTray = false;
|
||||||
let forceQuit = false;
|
let forceQuit = false;
|
||||||
let powerSaveBlockerId: null | number = null;
|
let powerSaveBlockerId: null | number = null;
|
||||||
let menuBuilder: MenuBuilder | null = null;
|
|
||||||
let currentPlaybackStatus: PlayerStatus = PlayerStatus.PAUSED;
|
|
||||||
let currentPrivateMode = false;
|
|
||||||
let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
|
||||||
let currentSidebarCollapsed = false;
|
|
||||||
let currentShuffleEnabled = false;
|
|
||||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
import('source-map-support').then((sourceMapSupport) => {
|
import('source-map-support').then((sourceMapSupport) => {
|
||||||
@@ -341,23 +333,6 @@ export const getMainWindow = () => {
|
|||||||
return mainWindow;
|
return mainWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rebuildMainMenu = () => {
|
|
||||||
if (!menuBuilder || !mainWindow) return;
|
|
||||||
|
|
||||||
menuBuilder.buildMenu({
|
|
||||||
accelerators: playbackMenuAccelerators,
|
|
||||||
playbackStatus: currentPlaybackStatus,
|
|
||||||
privateMode: currentPrivateMode,
|
|
||||||
repeatMode: currentRepeatMode,
|
|
||||||
shuffleEnabled: currentShuffleEnabled,
|
|
||||||
sidebarCollapsed: currentSidebarCollapsed,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
Menu.setApplicationMenu(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendToastToRenderer = ({
|
export const sendToastToRenderer = ({
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
@@ -724,8 +699,12 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
menuBuilder = new MenuBuilder(mainWindow);
|
const menuBuilder = new MenuBuilder(mainWindow);
|
||||||
rebuildMainMenu();
|
menuBuilder.buildMenu();
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
Menu.setApplicationMenu(null);
|
||||||
|
}
|
||||||
|
|
||||||
// Open URLs in the user's browser
|
// Open URLs in the user's browser
|
||||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||||
@@ -733,22 +712,6 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
|
||||||
desktopCapturer
|
|
||||||
.getSources({ types: ['screen'] })
|
|
||||||
.then((sources) => {
|
|
||||||
if (sources.length > 0) {
|
|
||||||
callback({ audio: 'loopback', video: sources[0] });
|
|
||||||
} else {
|
|
||||||
callback({});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
log.warn('desktopCapturer.getSources failed', err);
|
|
||||||
callback({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||||
new AppUpdater();
|
new AppUpdater();
|
||||||
}
|
}
|
||||||
@@ -819,17 +782,6 @@ enum BindingActions {
|
|||||||
VOLUME_UP = 'volumeUp',
|
VOLUME_UP = 'volumeUp',
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMenuAccelerator = (
|
|
||||||
data: Record<BindingActions, { allowGlobal: boolean; hotkey: string; isGlobal: boolean }>,
|
|
||||||
action: BindingActions,
|
|
||||||
) => {
|
|
||||||
const hotkey = data[action]?.hotkey;
|
|
||||||
|
|
||||||
if (!hotkey) return undefined;
|
|
||||||
|
|
||||||
return hotkeyToElectronAccelerator(hotkey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
||||||
[BindingActions.GLOBAL_SEARCH]: () => {},
|
[BindingActions.GLOBAL_SEARCH]: () => {},
|
||||||
[BindingActions.LOCAL_SEARCH]: () => {},
|
[BindingActions.LOCAL_SEARCH]: () => {},
|
||||||
@@ -883,26 +835,6 @@ ipcMain.on(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackMenuAccelerators = {
|
|
||||||
next: getMenuAccelerator(data, BindingActions.NEXT),
|
|
||||||
playPause:
|
|
||||||
getMenuAccelerator(data, BindingActions.PLAY_PAUSE) ||
|
|
||||||
getMenuAccelerator(data, BindingActions.PLAY) ||
|
|
||||||
getMenuAccelerator(data, BindingActions.PAUSE),
|
|
||||||
previous: getMenuAccelerator(data, BindingActions.PREVIOUS),
|
|
||||||
repeat: getMenuAccelerator(data, BindingActions.TOGGLE_REPEAT),
|
|
||||||
seekBackward: getMenuAccelerator(data, BindingActions.SKIP_BACKWARD),
|
|
||||||
seekForward: getMenuAccelerator(data, BindingActions.SKIP_FORWARD),
|
|
||||||
shuffle: getMenuAccelerator(data, BindingActions.SHUFFLE),
|
|
||||||
stop: getMenuAccelerator(data, BindingActions.STOP),
|
|
||||||
volumeDown: getMenuAccelerator(data, BindingActions.VOLUME_DOWN),
|
|
||||||
volumeUp: getMenuAccelerator(data, BindingActions.VOLUME_UP),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMacOS()) {
|
|
||||||
rebuildMainMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
|
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
|
||||||
|
|
||||||
if (globalMediaKeysEnabled) {
|
if (globalMediaKeysEnabled) {
|
||||||
@@ -1043,43 +975,3 @@ if (!ipcMain.eventNames().includes('open-application-directory')) {
|
|||||||
shell.openPath(userDataPath);
|
shell.openPath(userDataPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
|
||||||
currentPlaybackStatus = status;
|
|
||||||
|
|
||||||
if (!isMacOS()) return;
|
|
||||||
|
|
||||||
rebuildMainMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
|
|
||||||
currentRepeatMode = repeat;
|
|
||||||
|
|
||||||
if (!isMacOS()) return;
|
|
||||||
|
|
||||||
rebuildMainMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
|
||||||
currentShuffleEnabled = shuffle;
|
|
||||||
|
|
||||||
if (!isMacOS()) return;
|
|
||||||
|
|
||||||
rebuildMainMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('update-private-mode', (_event, privateMode: boolean) => {
|
|
||||||
currentPrivateMode = privateMode;
|
|
||||||
|
|
||||||
if (!isMacOS()) return;
|
|
||||||
|
|
||||||
rebuildMainMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('update-sidebar-collapsed', (_event, collapsedSidebar: boolean) => {
|
|
||||||
currentSidebarCollapsed = collapsedSidebar;
|
|
||||||
|
|
||||||
if (!isMacOS()) return;
|
|
||||||
|
|
||||||
rebuildMainMenu();
|
|
||||||
});
|
|
||||||
|
|||||||
+4
-190
@@ -1,53 +1,18 @@
|
|||||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
|
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
|
||||||
|
|
||||||
import packageJson from '../../package.json';
|
|
||||||
|
|
||||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export type MenuPlaybackState = {
|
|
||||||
accelerators?: {
|
|
||||||
next?: string;
|
|
||||||
playPause?: string;
|
|
||||||
previous?: string;
|
|
||||||
repeat?: string;
|
|
||||||
seekBackward?: string;
|
|
||||||
seekForward?: string;
|
|
||||||
shuffle?: string;
|
|
||||||
stop?: string;
|
|
||||||
volumeDown?: string;
|
|
||||||
volumeUp?: string;
|
|
||||||
};
|
|
||||||
playbackStatus?: PlayerStatus;
|
|
||||||
privateMode?: boolean;
|
|
||||||
repeatMode?: PlayerRepeat;
|
|
||||||
shuffleEnabled?: boolean;
|
|
||||||
sidebarCollapsed?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
||||||
selector?: string;
|
selector?: string;
|
||||||
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MenuBuilder {
|
export default class MenuBuilder {
|
||||||
developmentEnvironmentSetup = false;
|
|
||||||
mainWindow: BrowserWindow;
|
mainWindow: BrowserWindow;
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow) {
|
constructor(mainWindow: BrowserWindow) {
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDarwinTemplate({
|
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
||||||
accelerators,
|
|
||||||
playbackStatus = PlayerStatus.PAUSED,
|
|
||||||
privateMode = false,
|
|
||||||
repeatMode = PlayerRepeat.NONE,
|
|
||||||
shuffleEnabled = false,
|
|
||||||
sidebarCollapsed = false,
|
|
||||||
}: MenuPlaybackState = {}): MenuItemConstructorOptions[] {
|
|
||||||
const isPlaying = playbackStatus === PlayerStatus.PLAYING;
|
|
||||||
const isRepeatEnabled = repeatMode !== PlayerRepeat.NONE;
|
|
||||||
|
|
||||||
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
||||||
label: 'Electron',
|
label: 'Electron',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -64,21 +29,6 @@ export default class MenuBuilder {
|
|||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-open-manage-servers');
|
|
||||||
},
|
|
||||||
label: 'Manage servers',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
checked: privateMode,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-toggle-private-mode');
|
|
||||||
},
|
|
||||||
label: 'Private session',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ label: 'Services', submenu: [] },
|
{ label: 'Services', submenu: [] },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -121,22 +71,6 @@ export default class MenuBuilder {
|
|||||||
const subMenuViewDev: MenuItemConstructorOptions = {
|
const subMenuViewDev: MenuItemConstructorOptions = {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
|
||||||
accelerator: 'Command+K',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-open-command-palette');
|
|
||||||
},
|
|
||||||
label: 'Command Palette...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
checked: sidebarCollapsed,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-toggle-sidebar');
|
|
||||||
},
|
|
||||||
label: 'Collapse sidebar',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
{
|
||||||
accelerator: 'Command+R',
|
accelerator: 'Command+R',
|
||||||
click: () => {
|
click: () => {
|
||||||
@@ -163,22 +97,6 @@ export default class MenuBuilder {
|
|||||||
const subMenuViewProd: MenuItemConstructorOptions = {
|
const subMenuViewProd: MenuItemConstructorOptions = {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
|
||||||
accelerator: 'Command+K',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-open-command-palette');
|
|
||||||
},
|
|
||||||
label: 'Command Palette...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
checked: sidebarCollapsed,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-toggle-sidebar');
|
|
||||||
},
|
|
||||||
label: 'Collapse sidebar',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
{
|
||||||
accelerator: 'Ctrl+Command+F',
|
accelerator: 'Ctrl+Command+F',
|
||||||
click: () => {
|
click: () => {
|
||||||
@@ -201,89 +119,6 @@ export default class MenuBuilder {
|
|||||||
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const subMenuPlayback: MenuItemConstructorOptions = {
|
|
||||||
label: 'Playback',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.playPause,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-play-pause');
|
|
||||||
},
|
|
||||||
label: isPlaying ? 'Pause' : 'Play',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.next,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-next');
|
|
||||||
},
|
|
||||||
label: 'Next',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.previous,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-previous');
|
|
||||||
},
|
|
||||||
label: 'Previous',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.seekForward,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-skip-forward');
|
|
||||||
},
|
|
||||||
label: 'Seek Forward',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.seekBackward,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-skip-backward');
|
|
||||||
},
|
|
||||||
label: 'Seek Backforward',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.shuffle,
|
|
||||||
checked: shuffleEnabled,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-toggle-shuffle');
|
|
||||||
},
|
|
||||||
label: 'Shuffle',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.repeat,
|
|
||||||
checked: isRepeatEnabled,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-toggle-repeat');
|
|
||||||
},
|
|
||||||
label: 'Repeat',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.stop,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-stop');
|
|
||||||
},
|
|
||||||
label: 'Stop',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.volumeUp,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-volume-up');
|
|
||||||
},
|
|
||||||
label: 'Volume Up',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: accelerators?.volumeDown,
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-player-volume-down');
|
|
||||||
},
|
|
||||||
label: 'Volume Down',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const subMenuHelp: MenuItemConstructorOptions = {
|
const subMenuHelp: MenuItemConstructorOptions = {
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -313,13 +148,6 @@ export default class MenuBuilder {
|
|||||||
},
|
},
|
||||||
label: 'Search Issues',
|
label: 'Search Issues',
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.send('renderer-open-release-notes');
|
|
||||||
},
|
|
||||||
label: 'Version ' + packageJson.version,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,14 +156,7 @@ export default class MenuBuilder {
|
|||||||
? subMenuViewDev
|
? subMenuViewDev
|
||||||
: subMenuViewProd;
|
: subMenuViewProd;
|
||||||
|
|
||||||
return [
|
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||||
subMenuAbout,
|
|
||||||
subMenuEdit,
|
|
||||||
subMenuView,
|
|
||||||
subMenuPlayback,
|
|
||||||
subMenuWindow,
|
|
||||||
subMenuHelp,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDefaultTemplate(): MenuItemConstructorOptions[] {
|
buildDefaultTemplate(): MenuItemConstructorOptions[] {
|
||||||
@@ -441,14 +262,14 @@ export default class MenuBuilder {
|
|||||||
return templateDefault;
|
return templateDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMenu(playbackState: MenuPlaybackState = {}): Menu {
|
buildMenu(): Menu {
|
||||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||||
this.setupDevelopmentEnvironment();
|
this.setupDevelopmentEnvironment();
|
||||||
}
|
}
|
||||||
|
|
||||||
const template =
|
const template =
|
||||||
process.platform === 'darwin'
|
process.platform === 'darwin'
|
||||||
? this.buildDarwinTemplate(playbackState)
|
? this.buildDarwinTemplate()
|
||||||
: this.buildDefaultTemplate();
|
: this.buildDefaultTemplate();
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const menu = Menu.buildFromTemplate(template);
|
||||||
@@ -458,13 +279,6 @@ export default class MenuBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupDevelopmentEnvironment(): void {
|
setupDevelopmentEnvironment(): void {
|
||||||
// buildMenu can run multiple times as menu state updates; attach this once.
|
|
||||||
if (this.developmentEnvironmentSetup) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.developmentEnvironmentSetup = true;
|
|
||||||
|
|
||||||
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
||||||
const { x, y } = props;
|
const { x, y } = props;
|
||||||
|
|
||||||
|
|||||||
@@ -65,26 +65,6 @@ const rendererOpenSettings = (cb: (event: IpcRendererEvent) => void) => {
|
|||||||
ipcRenderer.on('renderer-open-settings', cb);
|
ipcRenderer.on('renderer-open-settings', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererOpenCommandPalette = (cb: (event: IpcRendererEvent) => void) => {
|
|
||||||
ipcRenderer.on('renderer-open-command-palette', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rendererOpenManageServers = (cb: (event: IpcRendererEvent) => void) => {
|
|
||||||
ipcRenderer.on('renderer-open-manage-servers', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rendererTogglePrivateMode = (cb: (event: IpcRendererEvent) => void) => {
|
|
||||||
ipcRenderer.on('renderer-toggle-private-mode', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rendererToggleSidebar = (cb: (event: IpcRendererEvent) => void) => {
|
|
||||||
ipcRenderer.on('renderer-toggle-sidebar', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rendererOpenReleaseNotes = (cb: (event: IpcRendererEvent) => void) => {
|
|
||||||
ipcRenderer.on('renderer-open-release-notes', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
disableAutoUpdates,
|
disableAutoUpdates,
|
||||||
@@ -98,12 +78,7 @@ export const utils = {
|
|||||||
openApplicationDirectory,
|
openApplicationDirectory,
|
||||||
openItem,
|
openItem,
|
||||||
playerErrorListener,
|
playerErrorListener,
|
||||||
rendererOpenCommandPalette,
|
|
||||||
rendererOpenManageServers,
|
|
||||||
rendererOpenReleaseNotes,
|
|
||||||
rendererOpenSettings,
|
rendererOpenSettings,
|
||||||
rendererTogglePrivateMode,
|
|
||||||
rendererToggleSidebar,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Utils = typeof utils;
|
export type Utils = typeof utils;
|
||||||
|
|||||||
@@ -297,8 +297,8 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
similarArtists:
|
similarArtists:
|
||||||
artistInfo?.similarArtist?.map((artist) => ({
|
artistInfo?.similarArtist?.map((artist) => ({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
imageId: artist.id,
|
imageId: null,
|
||||||
imageUrl: null,
|
imageUrl: artist?.artistImageUrl?.replace(/\?size=\d+/, '') ?? null,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
userFavorite: Boolean(artist.starred) || false,
|
userFavorite: Boolean(artist.starred) || false,
|
||||||
userRating: artist.userRating ?? null,
|
userRating: artist.userRating ?? null,
|
||||||
|
|||||||
@@ -237,27 +237,6 @@ function appendTranscodeParams(url: string, format?: string, bitrate?: number) {
|
|||||||
return streamUrl;
|
return streamUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGetTranscodeStreamUrl(
|
|
||||||
server: null | undefined | { credential?: string; url?: string },
|
|
||||||
args: {
|
|
||||||
mediaId: string;
|
|
||||||
mediaType: 'podcast' | 'song';
|
|
||||||
offset: number;
|
|
||||||
transcodeParams: string;
|
|
||||||
},
|
|
||||||
): string {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
c: 'Feishin',
|
|
||||||
mediaId: args.mediaId,
|
|
||||||
mediaType: args.mediaType,
|
|
||||||
offset: String(args.offset),
|
|
||||||
transcodeParams: args.transcodeParams,
|
|
||||||
v: '1.13.0',
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${server?.url}/rest/getTranscodeStream.view?${params.toString()}&${server?.credential}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortAndPaginate<T>(
|
function sortAndPaginate<T>(
|
||||||
items: T[],
|
items: T[],
|
||||||
options: {
|
options: {
|
||||||
@@ -508,7 +487,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
similarArtists:
|
similarArtists:
|
||||||
artistInfo?.similarArtist?.map((artist) => ({
|
artistInfo?.similarArtist?.map((artist) => ({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
imageId: artist.coverArt ?? artist.id,
|
imageId: null,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
userFavorite: Boolean(artist.starred) || false,
|
userFavorite: Boolean(artist.starred) || false,
|
||||||
@@ -2034,14 +2013,20 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
return appendTranscodeParams(streamUrl, format, bitrate);
|
return appendTranscodeParams(streamUrl, format, bitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transcodeStreamUrl = buildGetTranscodeStreamUrl(server, {
|
const transcodeStreamUrl = await ssApiClient(apiClientProps).getTranscodeStream({
|
||||||
mediaId: String(id),
|
query: {
|
||||||
mediaType: (mediaType ?? 'song') as 'podcast' | 'song',
|
mediaId: id,
|
||||||
offset: 0,
|
mediaType,
|
||||||
transcodeParams: td.transcodeParams,
|
offset: 0,
|
||||||
|
transcodeParams: td.transcodeParams,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return transcodeStreamUrl;
|
if (transcodeStreamUrl.status !== 200) {
|
||||||
|
throw new Error('Failed to get transcode stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
return transcodeStreamUrl.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
return streamUrl;
|
return streamUrl;
|
||||||
|
|||||||
+16
-4
@@ -10,9 +10,9 @@ import isElectron from 'is-electron';
|
|||||||
import { lazy, memo, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, memo, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
||||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||||
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
||||||
import { useNativeMenuSync } from '/@/renderer/hooks/use-native-menu-sync';
|
|
||||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
||||||
@@ -97,7 +97,7 @@ const AppEffects = () => (
|
|||||||
<CssSettingsEffect />
|
<CssSettingsEffect />
|
||||||
<GlobalShortcutsEffect />
|
<GlobalShortcutsEffect />
|
||||||
<LanguageEffect />
|
<LanguageEffect />
|
||||||
<NativeMenuSyncEffect />
|
<OpenSettingsEffect />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -170,8 +170,20 @@ const LanguageEffect = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NativeMenuSyncEffect = () => {
|
const OpenSettingsEffect = () => {
|
||||||
useNativeMenuSync();
|
useEffect(() => {
|
||||||
|
if (isElectron()) {
|
||||||
|
window.api.utils.rendererOpenSettings(() => {
|
||||||
|
openSettingsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipc?.removeAllListeners('renderer-open-settings');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
|
||||||
import { TableColumn } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const LAYOUT_FILL_COLUMN: ItemTableListColumnConfig = {
|
|
||||||
align: 'start',
|
|
||||||
autoSize: true,
|
|
||||||
id: TableColumn.LAYOUT_FILL,
|
|
||||||
isEnabled: true,
|
|
||||||
pinned: null,
|
|
||||||
width: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const appendLayoutFillColumn = (
|
|
||||||
columns: ItemTableListColumnConfig[],
|
|
||||||
autoFitColumns: boolean,
|
|
||||||
): ItemTableListColumnConfig[] => {
|
|
||||||
if (autoFitColumns || columns.length === 0) {
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unpinnedEnabled = columns.filter((c) => c.pinned === null && c.isEnabled !== false);
|
|
||||||
if (unpinnedEnabled.length === 0) {
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
if (unpinnedEnabled.some((c) => c.autoSize === true)) {
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...columns, LAYOUT_FILL_COLUMN];
|
|
||||||
};
|
|
||||||
@@ -40,13 +40,6 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
const setFavorite = useSetFavorite();
|
const setFavorite = useSetFavorite();
|
||||||
const setRating = useSetRating();
|
const setRating = useSetRating();
|
||||||
|
|
||||||
const playerRef = useRef(player);
|
|
||||||
const setFavoriteRef = useRef(setFavorite);
|
|
||||||
const setRatingRef = useRef(setRating);
|
|
||||||
playerRef.current = player;
|
|
||||||
setFavoriteRef.current = setFavorite;
|
|
||||||
setRatingRef.current = setRating;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigateRef.current = navigate;
|
navigateRef.current = navigate;
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@@ -273,14 +266,14 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.addToQueueByData(songsToAdd, playType, item.id);
|
player.addToQueueByData(songsToAdd, playType, item.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === LibraryItem.QUEUE_SONG) {
|
if (itemType === LibraryItem.QUEUE_SONG) {
|
||||||
const queueSong = item as QueueSong;
|
const queueSong = item as QueueSong;
|
||||||
if (queueSong._uniqueId) {
|
if (queueSong._uniqueId) {
|
||||||
playerRef.current.mediaPlay(queueSong._uniqueId);
|
player.mediaPlay(queueSong._uniqueId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -323,7 +316,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFavoriteRef.current(item._serverId, [item.id], apiItemType, favorite);
|
setFavorite(item._serverId, [item.id], apiItemType, favorite);
|
||||||
},
|
},
|
||||||
|
|
||||||
onMore: ({ event, internalState, item, itemType }: DefaultItemControlProps) => {
|
onMore: ({ event, internalState, item, itemType }: DefaultItemControlProps) => {
|
||||||
@@ -401,7 +394,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
|
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRating: ({
|
onRating: ({
|
||||||
@@ -427,12 +420,20 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
newRating = 0;
|
newRating = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRatingRef.current(item._serverId, [item.id], apiItemType, newRating);
|
setRating(item._serverId, [item.id], apiItemType, newRating);
|
||||||
},
|
},
|
||||||
|
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}, [enableMultiSelect, overrides, onColumnReordered, onColumnResized]);
|
}, [
|
||||||
|
enableMultiSelect,
|
||||||
|
overrides,
|
||||||
|
onColumnReordered,
|
||||||
|
onColumnResized,
|
||||||
|
player,
|
||||||
|
setFavorite,
|
||||||
|
setRating,
|
||||||
|
]);
|
||||||
|
|
||||||
return controls;
|
return controls;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -349,12 +349,9 @@ export const useItemListInfiniteLoader = ({
|
|||||||
mutationKey: getListRefreshMutationKey(eventKey),
|
mutationKey: getListRefreshMutationKey(eventKey),
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshMutationRef = useRef(refreshMutation);
|
|
||||||
refreshMutationRef.current = refreshMutation;
|
|
||||||
|
|
||||||
const refresh = useCallback(
|
const refresh = useCallback(
|
||||||
async (force?: boolean) => refreshMutationRef.current.mutateAsync(force),
|
async (force?: boolean) => refreshMutation.mutateAsync(force),
|
||||||
[],
|
[refreshMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateItems = useCallback(
|
const updateItems = useCallback(
|
||||||
@@ -386,7 +383,7 @@ export const useItemListInfiniteLoader = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMutationRef.current.mutate(true);
|
refreshMutation.mutate(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh);
|
eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh);
|
||||||
@@ -394,7 +391,7 @@ export const useItemListInfiniteLoader = ({
|
|||||||
return () => {
|
return () => {
|
||||||
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
||||||
};
|
};
|
||||||
}, [eventKey]);
|
}, [eventKey, refreshMutation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
useSuspenseQuery,
|
useSuspenseQuery,
|
||||||
UseSuspenseQueryOptions,
|
UseSuspenseQueryOptions,
|
||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
@@ -115,9 +115,6 @@ export const useItemListPaginatedLoader = ({
|
|||||||
mutationKey: getListRefreshMutationKey(eventKey ?? 'paginated'),
|
mutationKey: getListRefreshMutationKey(eventKey ?? 'paginated'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshMutationRef = useRef(refreshMutation);
|
|
||||||
refreshMutationRef.current = refreshMutation;
|
|
||||||
|
|
||||||
const updateItems = useCallback(
|
const updateItems = useCallback(
|
||||||
(indexes: number[], value: object) => {
|
(indexes: number[], value: object) => {
|
||||||
return queryClient.setQueryData(
|
return queryClient.setQueryData(
|
||||||
@@ -156,7 +153,7 @@ export const useItemListPaginatedLoader = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMutationRef.current.mutate(true);
|
refreshMutation.mutate(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
const handleFavorite = (payload: UserFavoriteEventPayload) => {
|
||||||
@@ -223,7 +220,7 @@ export const useItemListPaginatedLoader = ({
|
|||||||
eventEmitter.off('USER_FAVORITE', handleFavorite);
|
eventEmitter.off('USER_FAVORITE', handleFavorite);
|
||||||
eventEmitter.off('USER_RATING', handleRating);
|
eventEmitter.off('USER_RATING', handleRating);
|
||||||
};
|
};
|
||||||
}, [data, eventKey, itemType, serverId, updateItems]);
|
}, [data, eventKey, itemType, refreshMutation, serverId, updateItems]);
|
||||||
|
|
||||||
return { data: data?.items || [], pageCount, totalItemCount };
|
return { data: data?.items || [], pageCount, totalItemCount };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
|
|||||||
[TableColumn.ID]: null,
|
[TableColumn.ID]: null,
|
||||||
[TableColumn.IMAGE]: null,
|
[TableColumn.IMAGE]: null,
|
||||||
[TableColumn.LAST_PLAYED]: 'lastPlayedAt',
|
[TableColumn.LAST_PLAYED]: 'lastPlayedAt',
|
||||||
[TableColumn.LAYOUT_FILL]: null,
|
|
||||||
[TableColumn.OWNER]: null,
|
[TableColumn.OWNER]: null,
|
||||||
[TableColumn.PATH]: null,
|
[TableColumn.PATH]: null,
|
||||||
[TableColumn.PLAY_COUNT]: 'playCount',
|
[TableColumn.PLAY_COUNT]: 'playCount',
|
||||||
|
|||||||
@@ -179,14 +179,6 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle.resize-handle-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track-header-cell:hover .resize-handle.resize-handle-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle:hover {
|
.resize-handle:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -911,7 +911,8 @@ const DetailListHeaderCell = memo(
|
|||||||
const percent = col ? (columnWidthPercents[colIndex] ?? 0) : 0;
|
const percent = col ? (columnWidthPercents[colIndex] ?? 0) : 0;
|
||||||
const { fixedWidth, isFixedColumn } = getTrackColumnFixed(columnId);
|
const { fixedWidth, isFixedColumn } = getTrackColumnFixed(columnId);
|
||||||
const currentWidth = col?.width ?? (fixedWidth || 100);
|
const currentWidth = col?.width ?? (fixedWidth || 100);
|
||||||
const showResizeHandle = enableColumnResize && !isFixedColumn;
|
const showResizeHandle =
|
||||||
|
enableColumnResize && !isFixedColumn && !col?.autoSize && onColumnResized;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || !onColumnReordered) {
|
if (!containerRef.current || !onColumnReordered) {
|
||||||
@@ -1025,7 +1026,6 @@ const DetailListHeaderCell = memo(
|
|||||||
{showResizeHandle && (
|
{showResizeHandle && (
|
||||||
<DetailListColumnResizeHandle
|
<DetailListColumnResizeHandle
|
||||||
columnId={columnId}
|
columnId={columnId}
|
||||||
disabled={!!col?.autoSize}
|
|
||||||
initialWidth={currentWidth}
|
initialWidth={currentWidth}
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
side="right"
|
side="right"
|
||||||
@@ -1040,7 +1040,6 @@ DetailListHeaderCell.displayName = 'DetailListHeaderCell';
|
|||||||
|
|
||||||
interface DetailListColumnResizeHandleProps {
|
interface DetailListColumnResizeHandleProps {
|
||||||
columnId: TableColumn;
|
columnId: TableColumn;
|
||||||
disabled?: boolean;
|
|
||||||
initialWidth: number;
|
initialWidth: number;
|
||||||
onResize: (columnId: TableColumn, width: number) => void;
|
onResize: (columnId: TableColumn, width: number) => void;
|
||||||
side: 'left' | 'right';
|
side: 'left' | 'right';
|
||||||
@@ -1048,7 +1047,6 @@ interface DetailListColumnResizeHandleProps {
|
|||||||
|
|
||||||
const DetailListColumnResizeHandle = ({
|
const DetailListColumnResizeHandle = ({
|
||||||
columnId,
|
columnId,
|
||||||
disabled = false,
|
|
||||||
initialWidth,
|
initialWidth,
|
||||||
onResize,
|
onResize,
|
||||||
side,
|
side,
|
||||||
@@ -1093,11 +1091,6 @@ const DetailListColumnResizeHandle = ({
|
|||||||
}, [isDragging, columnId, onResize]);
|
}, [isDragging, columnId, onResize]);
|
||||||
|
|
||||||
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (disabled) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
@@ -1110,7 +1103,6 @@ const DetailListColumnResizeHandle = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.resizeHandle, {
|
className={clsx(styles.resizeHandle, {
|
||||||
[styles.resizeHandleDisabled]: disabled,
|
|
||||||
[styles.resizeHandleDragging]: isDragging,
|
[styles.resizeHandleDragging]: isDragging,
|
||||||
[styles.resizeHandleLeft]: side === 'left',
|
[styles.resizeHandleLeft]: side === 'left',
|
||||||
[styles.resizeHandleRight]: side === 'right',
|
[styles.resizeHandleRight]: side === 'right',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-block: var(--theme-spacing-xs);
|
|
||||||
padding-right: var(--theme-spacing-md);
|
padding-right: var(--theme-spacing-md);
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export interface ItemTableStickyLayoutOffsets {
|
||||||
|
inViewMarginTop: number;
|
||||||
|
stickyTop: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useItemTableStickyLayoutOffsets(): ItemTableStickyLayoutOffsets {
|
||||||
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
const isWinMac = windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS;
|
||||||
|
|
||||||
|
const [offsets, setOffsets] = useState(() => ({
|
||||||
|
inViewMarginTop: getFallbackInViewMargin(windowBarStyle),
|
||||||
|
stickyTop: getFallbackStickyTop(windowBarStyle),
|
||||||
|
}));
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const read = () => {
|
||||||
|
const topVar = isWinMac
|
||||||
|
? '--item-table-sticky-top-win-mac'
|
||||||
|
: '--item-table-sticky-top-default';
|
||||||
|
const marginVar = isWinMac
|
||||||
|
? '--item-table-sticky-inview-margin-win-mac'
|
||||||
|
: '--item-table-sticky-inview-margin-default';
|
||||||
|
setOffsets({
|
||||||
|
inViewMarginTop: resolveRootCssMarginLeftVar(
|
||||||
|
marginVar,
|
||||||
|
getFallbackInViewMargin(windowBarStyle),
|
||||||
|
),
|
||||||
|
stickyTop: resolveRootCssWidthVar(topVar, getFallbackStickyTop(windowBarStyle)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
read();
|
||||||
|
window.addEventListener('resize', read);
|
||||||
|
return () => window.removeEventListener('resize', read);
|
||||||
|
}, [isWinMac, windowBarStyle]);
|
||||||
|
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFallbackInViewMargin(windowBarStyle: Platform): number {
|
||||||
|
return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? -130 : -100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFallbackStickyTop(windowBarStyle: Platform): number {
|
||||||
|
return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRootCssMarginLeftVar(varName: string, fallback: number): number {
|
||||||
|
if (typeof document === 'undefined') return fallback;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.cssText = `position:fixed;left:0;top:0;margin-left:var(${varName});width:1px;height:0;margin-top:0;margin-right:0;margin-bottom:0;padding:0;border:none;visibility:hidden;pointer-events:none;`;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
const raw = getComputedStyle(el).marginLeft;
|
||||||
|
el.remove();
|
||||||
|
const v = parseFloat(raw);
|
||||||
|
return Number.isFinite(v) ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRootCssWidthVar(varName: string, fallback: number): number {
|
||||||
|
if (typeof document === 'undefined') return fallback;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.style.cssText = `position:fixed;left:-99999px;top:0;width:var(${varName});height:0;margin:0;padding:0;border:none;visibility:hidden;pointer-events:none;`;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
const w = el.getBoundingClientRect().width;
|
||||||
|
el.remove();
|
||||||
|
return Number.isFinite(w) && w > 0 ? w : fallback;
|
||||||
|
}
|
||||||
+9
-12
@@ -1,9 +1,8 @@
|
|||||||
|
import type { ItemTableStickyLayoutOffsets } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets';
|
||||||
|
|
||||||
import { useInView } from 'motion/react';
|
import { useInView } from 'motion/react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
|
||||||
import { Platform } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export interface GroupRowInfo {
|
export interface GroupRowInfo {
|
||||||
groupIndex: number;
|
groupIndex: number;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
@@ -18,6 +17,7 @@ export const useStickyTableGroupRows = ({
|
|||||||
mainGridRef,
|
mainGridRef,
|
||||||
shouldShowStickyHeader,
|
shouldShowStickyHeader,
|
||||||
stickyHeaderTop,
|
stickyHeaderTop,
|
||||||
|
stickyLayout,
|
||||||
}: {
|
}: {
|
||||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -27,17 +27,14 @@ export const useStickyTableGroupRows = ({
|
|||||||
mainGridRef: React.RefObject<HTMLDivElement | null>;
|
mainGridRef: React.RefObject<HTMLDivElement | null>;
|
||||||
shouldShowStickyHeader?: boolean;
|
shouldShowStickyHeader?: boolean;
|
||||||
stickyHeaderTop?: number;
|
stickyHeaderTop?: number;
|
||||||
|
stickyLayout: ItemTableStickyLayoutOffsets;
|
||||||
}) => {
|
}) => {
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { inViewMarginTop, stickyTop: layoutStickyTop } = stickyLayout;
|
||||||
const [stickyGroupIndex, setStickyGroupIndex] = useState<null | number>(null);
|
const [stickyGroupIndex, setStickyGroupIndex] = useState<null | number>(null);
|
||||||
|
|
||||||
const topMargin =
|
const groupRowsInViewMargin = `${inViewMarginTop}px 0px 0px 0px`;
|
||||||
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
|
|
||||||
? '-130px'
|
|
||||||
: '-100px';
|
|
||||||
|
|
||||||
const isTableInView = useInView(containerRef, {
|
const isTableInView = useInView(containerRef, {
|
||||||
margin: `${topMargin} 0px 0px 0px`,
|
margin: groupRowsInViewMargin as NonNullable<Parameters<typeof useInView>[1]>['margin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const stickyTop = useMemo(() => {
|
const stickyTop = useMemo(() => {
|
||||||
@@ -46,8 +43,8 @@ export const useStickyTableGroupRows = ({
|
|||||||
if (shouldShowStickyHeader && stickyHeaderTop !== undefined) {
|
if (shouldShowStickyHeader && stickyHeaderTop !== undefined) {
|
||||||
return stickyHeaderTop + headerHeight + 1;
|
return stickyHeaderTop + headerHeight + 1;
|
||||||
}
|
}
|
||||||
return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65;
|
return layoutStickyTop;
|
||||||
}, [windowBarStyle, shouldShowStickyHeader, stickyHeaderTop, headerHeight]);
|
}, [layoutStickyTop, shouldShowStickyHeader, stickyHeaderTop, headerHeight]);
|
||||||
|
|
||||||
// Calculate group row indexes
|
// Calculate group row indexes
|
||||||
const groupRowIndexes = useMemo(() => {
|
const groupRowIndexes = useMemo(() => {
|
||||||
|
|||||||
+12
-18
@@ -1,9 +1,8 @@
|
|||||||
|
import type { ItemTableStickyLayoutOffsets } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets';
|
||||||
|
|
||||||
import { useInView } from 'motion/react';
|
import { useInView } from 'motion/react';
|
||||||
import { RefObject, useEffect, useMemo, useRef } from 'react';
|
import { RefObject, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
|
||||||
import { Platform } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export const useStickyTableHeader = ({
|
export const useStickyTableHeader = ({
|
||||||
containerRef,
|
containerRef,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -12,6 +11,7 @@ export const useStickyTableHeader = ({
|
|||||||
pinnedLeftColumnRef,
|
pinnedLeftColumnRef,
|
||||||
pinnedRightColumnRef,
|
pinnedRightColumnRef,
|
||||||
stickyHeaderMainRef,
|
stickyHeaderMainRef,
|
||||||
|
stickyLayout,
|
||||||
}: {
|
}: {
|
||||||
containerRef: RefObject<HTMLDivElement | null>;
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -20,8 +20,9 @@ export const useStickyTableHeader = ({
|
|||||||
pinnedLeftColumnRef?: RefObject<HTMLDivElement | null>;
|
pinnedLeftColumnRef?: RefObject<HTMLDivElement | null>;
|
||||||
pinnedRightColumnRef?: RefObject<HTMLDivElement | null>;
|
pinnedRightColumnRef?: RefObject<HTMLDivElement | null>;
|
||||||
stickyHeaderMainRef?: RefObject<HTMLDivElement | null>;
|
stickyHeaderMainRef?: RefObject<HTMLDivElement | null>;
|
||||||
|
stickyLayout: ItemTableStickyLayoutOffsets;
|
||||||
}) => {
|
}) => {
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { inViewMarginTop, stickyTop } = stickyLayout;
|
||||||
const isScrollingRef = useRef({
|
const isScrollingRef = useRef({
|
||||||
main: false,
|
main: false,
|
||||||
pinnedLeft: false,
|
pinnedLeft: false,
|
||||||
@@ -29,27 +30,20 @@ export const useStickyTableHeader = ({
|
|||||||
stickyHeader: false,
|
stickyHeader: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const topMargin =
|
const inViewRootMargin = `${inViewMarginTop}px 0px 0px 0px`;
|
||||||
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
|
|
||||||
? '-130px'
|
|
||||||
: '-100px';
|
|
||||||
|
|
||||||
const isTableHeaderInView = useInView(headerRef, {
|
const inViewOptions = { margin: inViewRootMargin } as {
|
||||||
margin: `${topMargin} 0px 0px 0px`,
|
margin: NonNullable<Parameters<typeof useInView>[1]>['margin'];
|
||||||
});
|
};
|
||||||
|
|
||||||
const isTableInView = useInView(containerRef, {
|
const isTableHeaderInView = useInView(headerRef, inViewOptions);
|
||||||
margin: `${topMargin} 0px 0px 0px`,
|
|
||||||
});
|
const isTableInView = useInView(containerRef, inViewOptions);
|
||||||
|
|
||||||
const shouldShowStickyHeader = useMemo(() => {
|
const shouldShowStickyHeader = useMemo(() => {
|
||||||
return enabled && !isTableHeaderInView && isTableInView;
|
return enabled && !isTableHeaderInView && isTableInView;
|
||||||
}, [enabled, isTableHeaderInView, isTableInView]);
|
}, [enabled, isTableHeaderInView, isTableInView]);
|
||||||
|
|
||||||
const stickyTop = useMemo(() => {
|
|
||||||
return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65;
|
|
||||||
}, [windowBarStyle]);
|
|
||||||
|
|
||||||
// Sync scroll between sticky header and main grid/pinned columns
|
// Sync scroll between sticky header and main grid/pinned columns
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldShowStickyHeader || !stickyHeaderMainRef?.current || !mainGridRef?.current) {
|
if (!shouldShowStickyHeader || !stickyHeaderMainRef?.current || !mainGridRef?.current) {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { TableScrollShadowStore } from '/@/renderer/components/item-list/item-table-list/table-scroll-shadow-store';
|
|
||||||
|
|
||||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
@@ -20,7 +18,9 @@ export const useTablePaneSync = ({
|
|||||||
pinnedRowRef,
|
pinnedRowRef,
|
||||||
rowRef,
|
rowRef,
|
||||||
scrollContainerRef,
|
scrollContainerRef,
|
||||||
scrollShadowStore,
|
setShowLeftShadow,
|
||||||
|
setShowRightShadow,
|
||||||
|
setShowTopShadow,
|
||||||
}: {
|
}: {
|
||||||
enableDrag: boolean | undefined;
|
enableDrag: boolean | undefined;
|
||||||
enableDragScroll: boolean | undefined;
|
enableDragScroll: boolean | undefined;
|
||||||
@@ -36,7 +36,9 @@ export const useTablePaneSync = ({
|
|||||||
pinnedRowRef: React.RefObject<HTMLDivElement | null>;
|
pinnedRowRef: React.RefObject<HTMLDivElement | null>;
|
||||||
rowRef: React.RefObject<HTMLDivElement | null>;
|
rowRef: React.RefObject<HTMLDivElement | null>;
|
||||||
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
scrollShadowStore: TableScrollShadowStore;
|
setShowLeftShadow: (v: boolean) => void;
|
||||||
|
setShowRightShadow: (v: boolean) => void;
|
||||||
|
setShowTopShadow: (v: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
// Main grid overlayscrollbars - only handle X-axis if right-pinned columns exist
|
// Main grid overlayscrollbars - only handle X-axis if right-pinned columns exist
|
||||||
const [initialize, osInstance] = useOverlayScrollbars({
|
const [initialize, osInstance] = useOverlayScrollbars({
|
||||||
@@ -469,10 +471,8 @@ export const useTablePaneSync = ({
|
|||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
scrollShadowStore.setSnapshot({
|
setShowLeftShadow(false);
|
||||||
showLeftShadow: false,
|
setShowRightShadow(false);
|
||||||
showRightShadow: false,
|
|
||||||
});
|
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
@@ -482,10 +482,8 @@ export const useTablePaneSync = ({
|
|||||||
const scrollLeft = row.scrollLeft;
|
const scrollLeft = row.scrollLeft;
|
||||||
const maxScrollLeft = row.scrollWidth - row.clientWidth;
|
const maxScrollLeft = row.scrollWidth - row.clientWidth;
|
||||||
|
|
||||||
scrollShadowStore.setSnapshot({
|
setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0);
|
||||||
showLeftShadow: pinnedLeftColumnCount > 0 && scrollLeft > 0,
|
setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft);
|
||||||
showRightShadow: pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft,
|
|
||||||
});
|
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
checkScrollPosition();
|
checkScrollPosition();
|
||||||
@@ -496,7 +494,13 @@ export const useTablePaneSync = ({
|
|||||||
checkScrollPosition.cancel();
|
checkScrollPosition.cancel();
|
||||||
row.removeEventListener('scroll', checkScrollPosition);
|
row.removeEventListener('scroll', checkScrollPosition);
|
||||||
};
|
};
|
||||||
}, [pinnedLeftColumnCount, pinnedRightColumnCount, rowRef, scrollShadowStore]);
|
}, [
|
||||||
|
pinnedLeftColumnCount,
|
||||||
|
pinnedRightColumnCount,
|
||||||
|
rowRef,
|
||||||
|
setShowLeftShadow,
|
||||||
|
setShowRightShadow,
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle top shadow visibility based on vertical scroll
|
// Handle top shadow visibility based on vertical scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -505,7 +509,7 @@ export const useTablePaneSync = ({
|
|||||||
|
|
||||||
if (!row || !enableHeader) {
|
if (!row || !enableHeader) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
scrollShadowStore.setSnapshot({ showTopShadow: false });
|
setShowTopShadow(false);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
@@ -515,7 +519,7 @@ export const useTablePaneSync = ({
|
|||||||
|
|
||||||
const checkScrollPosition = throttle(() => {
|
const checkScrollPosition = throttle(() => {
|
||||||
const currentScrollTop = scrollElement.scrollTop;
|
const currentScrollTop = scrollElement.scrollTop;
|
||||||
scrollShadowStore.setSnapshot({ showTopShadow: currentScrollTop > 0 });
|
setShowTopShadow(currentScrollTop > 0);
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
checkScrollPosition();
|
checkScrollPosition();
|
||||||
@@ -526,5 +530,5 @@ export const useTablePaneSync = ({
|
|||||||
checkScrollPosition.cancel();
|
checkScrollPosition.cancel();
|
||||||
scrollElement.removeEventListener('scroll', checkScrollPosition);
|
scrollElement.removeEventListener('scroll', checkScrollPosition);
|
||||||
};
|
};
|
||||||
}, [enableHeader, pinnedRightColumnCount, pinnedRightColumnRef, rowRef, scrollShadowStore]);
|
}, [enableHeader, pinnedRightColumnCount, pinnedRightColumnRef, rowRef, setShowTopShadow]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -366,14 +366,6 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle.resize-handle-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container:hover .resize-handle.resize-handle-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle:hover {
|
.resize-handle:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table
|
|||||||
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
|
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
|
||||||
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
|
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
|
||||||
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
import { useItemTableListColumnResizeLive } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
|
||||||
import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types';
|
import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
@@ -194,14 +193,6 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === TableColumn.LAYOUT_FILL) {
|
|
||||||
return (
|
|
||||||
<TableColumnContainer {...props} {...dragProps} controls={controls} type={type}>
|
|
||||||
{null}
|
|
||||||
</TableColumnContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemType !== LibraryItem.FOLDER) {
|
if (itemType !== LibraryItem.FOLDER) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TableColumn.ACTIONS:
|
case TableColumn.ACTIONS:
|
||||||
@@ -716,8 +707,6 @@ export const TableColumnContainer = (
|
|||||||
|
|
||||||
interface ColumnResizeHandleProps {
|
interface ColumnResizeHandleProps {
|
||||||
columnId: TableColumn;
|
columnId: TableColumn;
|
||||||
columnIndex: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
initialWidth: number;
|
initialWidth: number;
|
||||||
onResize: (columnId: TableColumn, width: number) => void;
|
onResize: (columnId: TableColumn, width: number) => void;
|
||||||
side: 'left' | 'right';
|
side: 'left' | 'right';
|
||||||
@@ -725,8 +714,6 @@ interface ColumnResizeHandleProps {
|
|||||||
|
|
||||||
const ColumnResizeHandle = ({
|
const ColumnResizeHandle = ({
|
||||||
columnId,
|
columnId,
|
||||||
columnIndex,
|
|
||||||
disabled = false,
|
|
||||||
initialWidth,
|
initialWidth,
|
||||||
onResize,
|
onResize,
|
||||||
side,
|
side,
|
||||||
@@ -736,17 +723,6 @@ const ColumnResizeHandle = ({
|
|||||||
const startWidthRef = useRef<number>(initialWidth);
|
const startWidthRef = useRef<number>(initialWidth);
|
||||||
const startXRef = useRef<number>(0);
|
const startXRef = useRef<number>(0);
|
||||||
const finalWidthRef = useRef<number>(initialWidth);
|
const finalWidthRef = useRef<number>(initialWidth);
|
||||||
const columnResizeLive = useItemTableListColumnResizeLive();
|
|
||||||
const onResizeRef = useRef(onResize);
|
|
||||||
const columnResizeLiveRef = useRef(columnResizeLive);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onResizeRef.current = onResize;
|
|
||||||
}, [onResize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
columnResizeLiveRef.current = columnResizeLive;
|
|
||||||
}, [columnResizeLive]);
|
|
||||||
|
|
||||||
// Update the ref when initialWidth changes (but not during drag)
|
// Update the ref when initialWidth changes (but not during drag)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -762,7 +738,6 @@ const ColumnResizeHandle = ({
|
|||||||
const deltaX = event.clientX - startXRef.current;
|
const deltaX = event.clientX - startXRef.current;
|
||||||
const newWidth = Math.min(Math.max(10, startWidthRef.current + deltaX), 1000);
|
const newWidth = Math.min(Math.max(10, startWidthRef.current + deltaX), 1000);
|
||||||
finalWidthRef.current = newWidth;
|
finalWidthRef.current = newWidth;
|
||||||
columnResizeLiveRef.current?.scheduleColumnResizePreview(columnIndex, newWidth);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
@@ -771,8 +746,7 @@ const ColumnResizeHandle = ({
|
|||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = '';
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
onResizeRef.current(columnId, finalWidthRef.current);
|
onResize(columnId, finalWidthRef.current);
|
||||||
columnResizeLiveRef.current?.clearColumnResizePreview();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
@@ -781,18 +755,10 @@ const ColumnResizeHandle = ({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
document.body.style.cursor = '';
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
columnResizeLiveRef.current?.clearColumnResizePreview();
|
|
||||||
};
|
};
|
||||||
}, [isDragging, columnId, columnIndex]);
|
}, [isDragging, columnId, onResize]);
|
||||||
|
|
||||||
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (disabled) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
@@ -805,7 +771,6 @@ const ColumnResizeHandle = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.resizeHandle, {
|
className={clsx(styles.resizeHandle, {
|
||||||
[styles.resizeHandleDisabled]: disabled,
|
|
||||||
[styles.resizeHandleDragging]: isDragging,
|
[styles.resizeHandleDragging]: isDragging,
|
||||||
[styles.resizeHandleLeft]: side === 'left',
|
[styles.resizeHandleLeft]: side === 'left',
|
||||||
[styles.resizeHandleRight]: side === 'right',
|
[styles.resizeHandleRight]: side === 'right',
|
||||||
@@ -837,11 +802,7 @@ export const TableColumnHeaderContainer = (
|
|||||||
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null);
|
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!containerRef.current || !props.enableColumnReorder) {
|
||||||
!containerRef.current ||
|
|
||||||
!props.enableColumnReorder ||
|
|
||||||
props.type === TableColumn.LAYOUT_FILL
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,11 +917,9 @@ export const TableColumnHeaderContainer = (
|
|||||||
>
|
>
|
||||||
{columnLabelMap[props.type]}
|
{columnLabelMap[props.type]}
|
||||||
</Text>
|
</Text>
|
||||||
{props.enableColumnResize && (
|
{!columnConfig.autoSize && props.enableColumnResize && (
|
||||||
<ColumnResizeHandle
|
<ColumnResizeHandle
|
||||||
columnId={props.type}
|
columnId={props.type}
|
||||||
columnIndex={props.columnIndex}
|
|
||||||
disabled={!!columnConfig.autoSize}
|
|
||||||
initialWidth={currentWidth}
|
initialWidth={currentWidth}
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
side="right"
|
side="right"
|
||||||
@@ -1023,7 +982,6 @@ export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
|
|||||||
[TableColumn.LAST_PLAYED]: i18n.t('table.column.lastPlayed', {
|
[TableColumn.LAST_PLAYED]: i18n.t('table.column.lastPlayed', {
|
||||||
postProcess: 'upperCase',
|
postProcess: 'upperCase',
|
||||||
}) as string,
|
}) as string,
|
||||||
[TableColumn.LAYOUT_FILL]: '',
|
|
||||||
[TableColumn.OWNER]: i18n.t('table.column.owner', { postProcess: 'upperCase' }) as string,
|
[TableColumn.OWNER]: i18n.t('table.column.owner', { postProcess: 'upperCase' }) as string,
|
||||||
[TableColumn.PATH]: i18n.t('table.column.path', { postProcess: 'upperCase' }) as string,
|
[TableColumn.PATH]: i18n.t('table.column.path', { postProcess: 'upperCase' }) as string,
|
||||||
[TableColumn.PLAY_COUNT]: i18n.t('table.column.playCount', {
|
[TableColumn.PLAY_COUNT]: i18n.t('table.column.playCount', {
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
|
|
||||||
import React, {
|
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useSyncExternalStore } from 'react';
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
import type { TableItemProps } from './item-table-list';
|
import type { TableItemProps } from './item-table-list';
|
||||||
@@ -76,69 +68,6 @@ export const useItemTableListConfig = (): ItemTableListConfig | null => {
|
|||||||
return useContext(ItemTableListConfigContext);
|
return useContext(ItemTableListConfigContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ItemTableListColumnResizeLiveContextValue = {
|
|
||||||
clearColumnResizePreview: () => void;
|
|
||||||
scheduleColumnResizePreview: (columnIndex: number, width: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ItemTableListColumnResizeLiveContext =
|
|
||||||
createContext<ItemTableListColumnResizeLiveContextValue | null>(null);
|
|
||||||
|
|
||||||
export const ItemTableListColumnResizeLiveProvider = ({
|
|
||||||
children,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
value: ItemTableListColumnResizeLiveContextValue;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<ItemTableListColumnResizeLiveContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</ItemTableListColumnResizeLiveContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useItemTableListColumnResizeLive =
|
|
||||||
(): ItemTableListColumnResizeLiveContextValue | null => {
|
|
||||||
return useContext(ItemTableListColumnResizeLiveContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useItemTableListColumnResizeLiveState = () => {
|
|
||||||
const [columnResizePreview, setColumnResizePreview] = useState<null | {
|
|
||||||
columnIndex: number;
|
|
||||||
width: number;
|
|
||||||
}>(null);
|
|
||||||
const previewRafRef = useRef<null | number>(null);
|
|
||||||
const pendingPreviewRef = useRef<null | { columnIndex: number; width: number }>(null);
|
|
||||||
|
|
||||||
const scheduleColumnResizePreview = useCallback((columnIndex: number, width: number) => {
|
|
||||||
pendingPreviewRef.current = { columnIndex, width };
|
|
||||||
if (previewRafRef.current !== null) return;
|
|
||||||
previewRafRef.current = requestAnimationFrame(() => {
|
|
||||||
previewRafRef.current = null;
|
|
||||||
const pending = pendingPreviewRef.current;
|
|
||||||
if (pending) {
|
|
||||||
setColumnResizePreview(pending);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearColumnResizePreview = useCallback(() => {
|
|
||||||
if (previewRafRef.current !== null) {
|
|
||||||
cancelAnimationFrame(previewRafRef.current);
|
|
||||||
previewRafRef.current = null;
|
|
||||||
}
|
|
||||||
pendingPreviewRef.current = null;
|
|
||||||
setColumnResizePreview(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
clearColumnResizePreview,
|
|
||||||
columnResizePreview,
|
|
||||||
scheduleColumnResizePreview,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type ItemTableListStoreContextValue = {
|
type ItemTableListStoreContextValue = {
|
||||||
activeRowStore: ActiveRowStore;
|
activeRowStore: ActiveRowStore;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
.item-table-pinned-rows-grid-container.header-fixed {
|
.item-table-pinned-rows-grid-container.header-fixed {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: 65px;
|
top: var(--item-table-sticky-top-default);
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
background-color: var(--theme-bg-primary);
|
background-color: var(--theme-bg-primary);
|
||||||
box-shadow: 0 -1px 0 0 var(--theme-colors-border);
|
box-shadow: 0 -1px 0 0 var(--theme-colors-border);
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-table-pinned-rows-grid-container.header-window-bar {
|
.item-table-pinned-rows-grid-container.header-window-bar {
|
||||||
top: 95px;
|
top: var(--item-table-sticky-top-win-mac);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-table-list-container.header-fixed-margin {
|
.item-table-list-container.header-fixed-margin {
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
z-index: 15;
|
z-index: 15;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-color: var(--theme-colors-background);
|
background-color: var(--theme-colors-background);
|
||||||
border-bottom: 1px solid var(--theme-colors-border);
|
border-bottom: 1px solid var(--theme-colors-border);
|
||||||
@@ -168,7 +168,6 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-x: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-scrollbar {
|
.no-scrollbar {
|
||||||
@@ -179,10 +178,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-table-container :global(.os-scrollbar) {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-table-pinned-header-shadow {
|
.item-table-pinned-header-shadow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
useSyncExternalStore,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { type CellComponentProps, Grid } from 'react-window-v2';
|
import { type CellComponentProps, Grid } from 'react-window-v2';
|
||||||
|
|
||||||
import styles from './item-table-list.module.css';
|
import styles from './item-table-list.module.css';
|
||||||
|
|
||||||
import { appendLayoutFillColumn } from '/@/renderer/components/item-list/helpers/append-layout-fill-column';
|
|
||||||
import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id';
|
import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id';
|
||||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +31,7 @@ import { parseTableColumns } from '/@/renderer/components/item-list/helpers/pars
|
|||||||
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
||||||
import { useContainerWidthTracking } from '/@/renderer/components/item-list/item-table-list/hooks/use-container-width-tracking';
|
import { useContainerWidthTracking } from '/@/renderer/components/item-list/item-table-list/hooks/use-container-width-tracking';
|
||||||
import { useRowInteractionDelegate } from '/@/renderer/components/item-list/item-table-list/hooks/use-row-interaction-delegate';
|
import { useRowInteractionDelegate } from '/@/renderer/components/item-list/item-table-list/hooks/use-row-interaction-delegate';
|
||||||
|
import { useItemTableStickyLayoutOffsets } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-table-sticky-layout-offsets';
|
||||||
import { useStickyGroupRowPositioning } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-group-row-positioning';
|
import { useStickyGroupRowPositioning } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-group-row-positioning';
|
||||||
import { useStickyHeaderPositioning } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-header-positioning';
|
import { useStickyHeaderPositioning } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-header-positioning';
|
||||||
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
||||||
@@ -46,20 +45,14 @@ import { useTableRowModel } from '/@/renderer/components/item-list/item-table-li
|
|||||||
import { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index';
|
import { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index';
|
||||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
import {
|
import {
|
||||||
ItemTableListColumnResizeLiveProvider,
|
|
||||||
type ItemTableListConfig,
|
type ItemTableListConfig,
|
||||||
ItemTableListConfigProvider,
|
ItemTableListConfigProvider,
|
||||||
ItemTableListStoreProvider,
|
ItemTableListStoreProvider,
|
||||||
useItemTableListColumnResizeLiveState,
|
|
||||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||||
import {
|
import {
|
||||||
MemoizedCellRouter,
|
MemoizedCellRouter,
|
||||||
useColumnCellComponents,
|
useColumnCellComponents,
|
||||||
} from '/@/renderer/components/item-list/item-table-list/memoized-cell-router';
|
} from '/@/renderer/components/item-list/item-table-list/memoized-cell-router';
|
||||||
import {
|
|
||||||
createTableScrollShadowStore,
|
|
||||||
type TableScrollShadowStore,
|
|
||||||
} from '/@/renderer/components/item-list/item-table-list/table-scroll-shadow-store';
|
|
||||||
import {
|
import {
|
||||||
ItemControls,
|
ItemControls,
|
||||||
ItemListHandle,
|
ItemListHandle,
|
||||||
@@ -111,63 +104,6 @@ export enum TableItemSize {
|
|||||||
LARGE = 88,
|
LARGE = 88,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemTableScrollShadowTop = memo(function ItemTableScrollShadowTop({
|
|
||||||
enableHeader,
|
|
||||||
enableScrollShadow,
|
|
||||||
scrollShadowStore,
|
|
||||||
}: {
|
|
||||||
enableHeader: boolean;
|
|
||||||
enableScrollShadow: boolean;
|
|
||||||
scrollShadowStore: TableScrollShadowStore;
|
|
||||||
}) {
|
|
||||||
const { showTopShadow } = useSyncExternalStore(
|
|
||||||
scrollShadowStore.subscribe,
|
|
||||||
scrollShadowStore.getSnapshot,
|
|
||||||
);
|
|
||||||
if (!enableHeader || !enableScrollShadow || !showTopShadow) return null;
|
|
||||||
return <div className={styles.itemTableTopScrollShadow} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
ItemTableScrollShadowTop.displayName = 'ItemTableScrollShadowTop';
|
|
||||||
|
|
||||||
const ItemTableScrollShadowLeft = memo(function ItemTableScrollShadowLeft({
|
|
||||||
enableScrollShadow,
|
|
||||||
pinnedLeftColumnCount,
|
|
||||||
scrollShadowStore,
|
|
||||||
}: {
|
|
||||||
enableScrollShadow: boolean;
|
|
||||||
pinnedLeftColumnCount: number;
|
|
||||||
scrollShadowStore: TableScrollShadowStore;
|
|
||||||
}) {
|
|
||||||
const { showLeftShadow } = useSyncExternalStore(
|
|
||||||
scrollShadowStore.subscribe,
|
|
||||||
scrollShadowStore.getSnapshot,
|
|
||||||
);
|
|
||||||
if (pinnedLeftColumnCount <= 0 || !enableScrollShadow || !showLeftShadow) return null;
|
|
||||||
return <div className={styles.itemTableLeftScrollShadow} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
ItemTableScrollShadowLeft.displayName = 'ItemTableScrollShadowLeft';
|
|
||||||
|
|
||||||
const ItemTableScrollShadowRight = memo(function ItemTableScrollShadowRight({
|
|
||||||
enableScrollShadow,
|
|
||||||
pinnedRightColumnCount,
|
|
||||||
scrollShadowStore,
|
|
||||||
}: {
|
|
||||||
enableScrollShadow: boolean;
|
|
||||||
pinnedRightColumnCount: number;
|
|
||||||
scrollShadowStore: TableScrollShadowStore;
|
|
||||||
}) {
|
|
||||||
const { showRightShadow } = useSyncExternalStore(
|
|
||||||
scrollShadowStore.subscribe,
|
|
||||||
scrollShadowStore.getSnapshot,
|
|
||||||
);
|
|
||||||
if (pinnedRightColumnCount <= 0 || !enableScrollShadow || !showRightShadow) return null;
|
|
||||||
return <div className={styles.itemTableRightScrollShadow} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight';
|
|
||||||
|
|
||||||
interface VirtualizedTableGridProps {
|
interface VirtualizedTableGridProps {
|
||||||
calculatedColumnWidths: number[];
|
calculatedColumnWidths: number[];
|
||||||
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
|
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
|
||||||
@@ -185,7 +121,9 @@ interface VirtualizedTableGridProps {
|
|||||||
pinnedRightColumnRef: React.RefObject<HTMLDivElement | null>;
|
pinnedRightColumnRef: React.RefObject<HTMLDivElement | null>;
|
||||||
pinnedRowCount: number;
|
pinnedRowCount: number;
|
||||||
pinnedRowRef: React.RefObject<HTMLDivElement | null>;
|
pinnedRowRef: React.RefObject<HTMLDivElement | null>;
|
||||||
scrollShadowStore: TableScrollShadowStore;
|
showLeftShadow: boolean;
|
||||||
|
showRightShadow: boolean;
|
||||||
|
showTopShadow: boolean;
|
||||||
tableConfig: ItemTableListConfig;
|
tableConfig: ItemTableListConfig;
|
||||||
totalColumnCount: number;
|
totalColumnCount: number;
|
||||||
totalRowCount: number;
|
totalRowCount: number;
|
||||||
@@ -208,7 +146,9 @@ const VirtualizedTableGrid = ({
|
|||||||
pinnedRightColumnRef,
|
pinnedRightColumnRef,
|
||||||
pinnedRowCount,
|
pinnedRowCount,
|
||||||
pinnedRowRef,
|
pinnedRowRef,
|
||||||
scrollShadowStore,
|
showLeftShadow,
|
||||||
|
showRightShadow,
|
||||||
|
showTopShadow,
|
||||||
tableConfig,
|
tableConfig,
|
||||||
totalColumnCount,
|
totalColumnCount,
|
||||||
totalRowCount,
|
totalRowCount,
|
||||||
@@ -544,7 +484,7 @@ const VirtualizedTableGrid = ({
|
|||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
minHeight: `${pinnedRowsMinHeightPx}px`,
|
minHeight: `${pinnedRowsMinHeightPx}px`,
|
||||||
overflow: 'visible',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
@@ -558,11 +498,9 @@ const VirtualizedTableGrid = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ItemTableScrollShadowTop
|
{enableHeader && enableScrollShadow && showTopShadow && (
|
||||||
enableHeader={!!enableHeader}
|
<div className={styles.itemTableTopScrollShadow} />
|
||||||
enableScrollShadow={enableScrollShadow}
|
)}
|
||||||
scrollShadowStore={scrollShadowStore}
|
|
||||||
/>
|
|
||||||
{!!pinnedLeftColumnCount && (
|
{!!pinnedLeftColumnCount && (
|
||||||
<div
|
<div
|
||||||
className={styles.itemTablePinnedColumnsContainer}
|
className={styles.itemTablePinnedColumnsContainer}
|
||||||
@@ -617,11 +555,9 @@ const VirtualizedTableGrid = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ItemTableScrollShadowTop
|
{enableHeader && enableScrollShadow && showTopShadow && (
|
||||||
enableHeader={!!enableHeader}
|
<div className={styles.itemTableTopScrollShadow} />
|
||||||
enableScrollShadow={enableScrollShadow}
|
)}
|
||||||
scrollShadowStore={scrollShadowStore}
|
|
||||||
/>
|
|
||||||
<div className={styles.itemTableGridContainer} ref={mergedRowRef}>
|
<div className={styles.itemTableGridContainer} ref={mergedRowRef}>
|
||||||
<Grid
|
<Grid
|
||||||
cellComponent={RowCell}
|
cellComponent={RowCell}
|
||||||
@@ -633,16 +569,12 @@ const VirtualizedTableGrid = ({
|
|||||||
rowCount={totalRowCount}
|
rowCount={totalRowCount}
|
||||||
rowHeight={rowHeightMemoized}
|
rowHeight={rowHeightMemoized}
|
||||||
/>
|
/>
|
||||||
<ItemTableScrollShadowLeft
|
{pinnedLeftColumnCount > 0 && enableScrollShadow && showLeftShadow && (
|
||||||
enableScrollShadow={enableScrollShadow}
|
<div className={styles.itemTableLeftScrollShadow} />
|
||||||
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
)}
|
||||||
scrollShadowStore={scrollShadowStore}
|
{pinnedRightColumnCount > 0 && enableScrollShadow && showRightShadow && (
|
||||||
/>
|
<div className={styles.itemTableRightScrollShadow} />
|
||||||
<ItemTableScrollShadowRight
|
)}
|
||||||
enableScrollShadow={enableScrollShadow}
|
|
||||||
pinnedRightColumnCount={pinnedRightColumnCount}
|
|
||||||
scrollShadowStore={scrollShadowStore}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!pinnedRightColumnCount && (
|
{!!pinnedRightColumnCount && (
|
||||||
@@ -662,7 +594,7 @@ const VirtualizedTableGrid = ({
|
|||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
minHeight: `${pinnedRowsMinHeightPx}px`,
|
minHeight: `${pinnedRowsMinHeightPx}px`,
|
||||||
overflow: 'visible',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
@@ -680,11 +612,9 @@ const VirtualizedTableGrid = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ItemTableScrollShadowTop
|
{enableHeader && enableScrollShadow && showTopShadow && (
|
||||||
enableHeader={!!enableHeader}
|
<div className={styles.itemTableTopScrollShadow} />
|
||||||
enableScrollShadow={enableScrollShadow}
|
)}
|
||||||
scrollShadowStore={scrollShadowStore}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
className={styles.itemTablePinnedRightColumnsContainer}
|
className={styles.itemTablePinnedRightColumnsContainer}
|
||||||
ref={pinnedRightColumnRef}
|
ref={pinnedRightColumnRef}
|
||||||
@@ -737,7 +667,9 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
|
|||||||
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
|
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
|
||||||
prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
|
prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
|
||||||
prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
|
prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
|
||||||
prevProps.scrollShadowStore === nextProps.scrollShadowStore &&
|
prevProps.showLeftShadow === nextProps.showLeftShadow &&
|
||||||
|
prevProps.showRightShadow === nextProps.showRightShadow &&
|
||||||
|
prevProps.showTopShadow === nextProps.showTopShadow &&
|
||||||
prevProps.totalColumnCount === nextProps.totalColumnCount &&
|
prevProps.totalColumnCount === nextProps.totalColumnCount &&
|
||||||
prevProps.totalRowCount === nextProps.totalRowCount &&
|
prevProps.totalRowCount === nextProps.totalRowCount &&
|
||||||
prevProps.CellComponent === nextProps.CellComponent
|
prevProps.CellComponent === nextProps.CellComponent
|
||||||
@@ -898,6 +830,8 @@ const ItemTableListStickyUI = memo(
|
|||||||
const stickyHeaderMainRef = useRef<HTMLDivElement | null>(null);
|
const stickyHeaderMainRef = useRef<HTMLDivElement | null>(null);
|
||||||
const stickyHeaderRightRef = useRef<HTMLDivElement | null>(null);
|
const stickyHeaderRightRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const stickyLayout = useItemTableStickyLayoutOffsets();
|
||||||
|
|
||||||
const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({
|
const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({
|
||||||
containerRef,
|
containerRef,
|
||||||
enabled: enableHeader && enableStickyHeader,
|
enabled: enableHeader && enableStickyHeader,
|
||||||
@@ -906,6 +840,7 @@ const ItemTableListStickyUI = memo(
|
|||||||
pinnedLeftColumnRef,
|
pinnedLeftColumnRef,
|
||||||
pinnedRightColumnRef,
|
pinnedRightColumnRef,
|
||||||
stickyHeaderMainRef,
|
stickyHeaderMainRef,
|
||||||
|
stickyLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
useStickyHeaderPositioning({
|
useStickyHeaderPositioning({
|
||||||
@@ -927,6 +862,7 @@ const ItemTableListStickyUI = memo(
|
|||||||
mainGridRef: rowRef,
|
mainGridRef: rowRef,
|
||||||
shouldShowStickyHeader,
|
shouldShowStickyHeader,
|
||||||
stickyHeaderTop: stickyTop,
|
stickyHeaderTop: stickyTop,
|
||||||
|
stickyLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldRenderStickyGroupRow = shouldShowStickyGroupRow;
|
const shouldRenderStickyGroupRow = shouldShowStickyGroupRow;
|
||||||
@@ -971,7 +907,7 @@ const ItemTableListStickyUI = memo(
|
|||||||
style={{
|
style={{
|
||||||
flex: '0 1 auto',
|
flex: '0 1 auto',
|
||||||
minWidth: `${pinnedLeftWidth}px`,
|
minWidth: `${pinnedLeftWidth}px`,
|
||||||
overflow: 'visible',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{parsedColumns
|
{parsedColumns
|
||||||
@@ -1055,7 +991,7 @@ const ItemTableListStickyUI = memo(
|
|||||||
style={{
|
style={{
|
||||||
flex: '0 1 auto',
|
flex: '0 1 auto',
|
||||||
minWidth: `${pinnedRightWidth}px`,
|
minWidth: `${pinnedRightWidth}px`,
|
||||||
overflow: 'visible',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{parsedColumns
|
{parsedColumns
|
||||||
@@ -1279,11 +1215,6 @@ const BaseItemTableList = ({
|
|||||||
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
||||||
const [totalContainerWidth, setTotalContainerWidth] = useState(0);
|
const [totalContainerWidth, setTotalContainerWidth] = useState(0);
|
||||||
|
|
||||||
const columnsForLayout = useMemo(
|
|
||||||
() => appendLayoutFillColumn(columns, autoFitColumns),
|
|
||||||
[autoFitColumns, columns],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
parsedColumns,
|
parsedColumns,
|
||||||
@@ -1293,33 +1224,9 @@ const BaseItemTableList = ({
|
|||||||
} = useTableColumnModel({
|
} = useTableColumnModel({
|
||||||
autoFitColumns,
|
autoFitColumns,
|
||||||
centerContainerWidth,
|
centerContainerWidth,
|
||||||
columns: columnsForLayout,
|
columns,
|
||||||
totalContainerWidth,
|
totalContainerWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { clearColumnResizePreview, columnResizePreview, scheduleColumnResizePreview } =
|
|
||||||
useItemTableListColumnResizeLiveState();
|
|
||||||
|
|
||||||
const columnResizeLiveValue = useMemo(
|
|
||||||
() => ({
|
|
||||||
clearColumnResizePreview,
|
|
||||||
scheduleColumnResizePreview,
|
|
||||||
}),
|
|
||||||
[clearColumnResizePreview, scheduleColumnResizePreview],
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayColumnWidths = useMemo(() => {
|
|
||||||
if (!columnResizePreview) {
|
|
||||||
return calculatedColumnWidths;
|
|
||||||
}
|
|
||||||
const next = calculatedColumnWidths.slice();
|
|
||||||
const { columnIndex, width } = columnResizePreview;
|
|
||||||
if (columnIndex >= 0 && columnIndex < next.length) {
|
|
||||||
next[columnIndex] = width;
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}, [calculatedColumnWidths, columnResizePreview]);
|
|
||||||
|
|
||||||
const playerContext = usePlayer();
|
const playerContext = usePlayer();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -1355,7 +1262,9 @@ const BaseItemTableList = ({
|
|||||||
const pinnedRightColumnRef = useRef<HTMLDivElement>(null);
|
const pinnedRightColumnRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mergedRowRef = useMergedRef(rowRef, scrollContainerRef);
|
const mergedRowRef = useMergedRef(rowRef, scrollContainerRef);
|
||||||
const scrollShadowStore = useMemo(() => createTableScrollShadowStore(), []);
|
const [showLeftShadow, setShowLeftShadow] = useState(false);
|
||||||
|
const [showRightShadow, setShowRightShadow] = useState(false);
|
||||||
|
const [showTopShadow, setShowTopShadow] = useState(false);
|
||||||
const handleRef = useRef<ItemListHandle | null>(null);
|
const handleRef = useRef<ItemListHandle | null>(null);
|
||||||
const { focused, ref: focusRef } = useFocusWithin();
|
const { focused, ref: focusRef } = useFocusWithin();
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -1413,7 +1322,9 @@ const BaseItemTableList = ({
|
|||||||
pinnedRowRef,
|
pinnedRowRef,
|
||||||
rowRef,
|
rowRef,
|
||||||
scrollContainerRef,
|
scrollContainerRef,
|
||||||
scrollShadowStore,
|
setShowLeftShadow,
|
||||||
|
setShowRightShadow,
|
||||||
|
setShowTopShadow,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRowHeight = useCallback(
|
const getRowHeight = useCallback(
|
||||||
@@ -1537,7 +1448,7 @@ const BaseItemTableList = ({
|
|||||||
// Create itemProps for sticky header
|
// Create itemProps for sticky header
|
||||||
const stickyHeaderItemProps: TableItemProps = useMemo(
|
const stickyHeaderItemProps: TableItemProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
calculatedColumnWidths: displayColumnWidths,
|
calculatedColumnWidths,
|
||||||
cellPadding,
|
cellPadding,
|
||||||
columns: parsedColumns,
|
columns: parsedColumns,
|
||||||
controls,
|
controls,
|
||||||
@@ -1557,9 +1468,9 @@ const BaseItemTableList = ({
|
|||||||
internalState,
|
internalState,
|
||||||
itemType,
|
itemType,
|
||||||
pinnedLeftColumnCount,
|
pinnedLeftColumnCount,
|
||||||
pinnedLeftColumnWidths: displayColumnWidths.slice(0, pinnedLeftColumnCount),
|
pinnedLeftColumnWidths: calculatedColumnWidths.slice(0, pinnedLeftColumnCount),
|
||||||
pinnedRightColumnCount,
|
pinnedRightColumnCount,
|
||||||
pinnedRightColumnWidths: displayColumnWidths.slice(
|
pinnedRightColumnWidths: calculatedColumnWidths.slice(
|
||||||
pinnedLeftColumnCount + totalColumnCount,
|
pinnedLeftColumnCount + totalColumnCount,
|
||||||
),
|
),
|
||||||
playerContext,
|
playerContext,
|
||||||
@@ -1568,7 +1479,7 @@ const BaseItemTableList = ({
|
|||||||
tableId,
|
tableId,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
displayColumnWidths,
|
calculatedColumnWidths,
|
||||||
cellPadding,
|
cellPadding,
|
||||||
controls,
|
controls,
|
||||||
parsedColumns,
|
parsedColumns,
|
||||||
@@ -1673,81 +1584,73 @@ const BaseItemTableList = ({
|
|||||||
};
|
};
|
||||||
}, [CellComponent, columnCellComponents]);
|
}, [CellComponent, columnCellComponents]);
|
||||||
|
|
||||||
const tableMotion = (
|
|
||||||
<motion.div
|
|
||||||
className={styles.itemTableListContainer}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
const element = e.currentTarget as HTMLDivElement;
|
|
||||||
// Focus without scrolling into view
|
|
||||||
if (element.focus) {
|
|
||||||
element.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={mergedContainerRef}
|
|
||||||
tabIndex={0}
|
|
||||||
{...animationProps.fadeIn}
|
|
||||||
transition={{ duration: enableEntranceAnimation ? 0.3 : 0, ease: 'anticipate' }}
|
|
||||||
>
|
|
||||||
<ItemTableListStickyUI
|
|
||||||
calculatedColumnWidths={displayColumnWidths}
|
|
||||||
CellComponent={optimizedCellComponent}
|
|
||||||
containerRef={containerRef}
|
|
||||||
data={data}
|
|
||||||
enableHeader={!!enableHeader}
|
|
||||||
enableStickyGroupRows={!!enableStickyGroupRows}
|
|
||||||
enableStickyHeader={!!enableStickyHeader}
|
|
||||||
getRowHeightWrapper={getRowHeightWrapper}
|
|
||||||
groups={groups}
|
|
||||||
headerHeight={headerHeight}
|
|
||||||
internalState={internalState}
|
|
||||||
parsedColumns={parsedColumns}
|
|
||||||
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
|
||||||
pinnedLeftColumnRef={pinnedLeftColumnRef}
|
|
||||||
pinnedRightColumnCount={pinnedRightColumnCount}
|
|
||||||
pinnedRightColumnRef={pinnedRightColumnRef}
|
|
||||||
pinnedRowRef={pinnedRowRef}
|
|
||||||
rowHeight={rowHeight}
|
|
||||||
rowRef={rowRef}
|
|
||||||
size={size}
|
|
||||||
stickyHeaderItemProps={stickyHeaderItemProps}
|
|
||||||
totalColumnCount={totalColumnCount}
|
|
||||||
/>
|
|
||||||
<MemoizedVirtualizedTableGrid
|
|
||||||
calculatedColumnWidths={displayColumnWidths}
|
|
||||||
CellComponent={optimizedCellComponent}
|
|
||||||
data={data}
|
|
||||||
dataWithGroups={dataWithGroups}
|
|
||||||
enableScrollShadow={enableScrollShadow}
|
|
||||||
getItem={getItem}
|
|
||||||
headerHeight={headerHeight}
|
|
||||||
mergedRowRef={mergedRowRef}
|
|
||||||
onRangeChanged={onRangeChanged}
|
|
||||||
parsedColumns={parsedColumns}
|
|
||||||
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
|
||||||
pinnedLeftColumnRef={pinnedLeftColumnRef}
|
|
||||||
pinnedRightColumnCount={pinnedRightColumnCount}
|
|
||||||
pinnedRightColumnRef={pinnedRightColumnRef}
|
|
||||||
pinnedRowCount={pinnedRowCount}
|
|
||||||
pinnedRowRef={pinnedRowRef}
|
|
||||||
scrollShadowStore={scrollShadowStore}
|
|
||||||
tableConfig={tableConfigValue}
|
|
||||||
totalColumnCount={totalColumnCount}
|
|
||||||
totalRowCount={totalRowCount}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemTableListStoreProvider activeRowId={activeRowId}>
|
<ItemTableListStoreProvider activeRowId={activeRowId}>
|
||||||
<ItemTableListConfigProvider value={tableConfigValue}>
|
<ItemTableListConfigProvider value={tableConfigValue}>
|
||||||
{onColumnResized ? (
|
<motion.div
|
||||||
<ItemTableListColumnResizeLiveProvider value={columnResizeLiveValue}>
|
className={styles.itemTableListContainer}
|
||||||
{tableMotion}
|
onKeyDown={handleKeyDown}
|
||||||
</ItemTableListColumnResizeLiveProvider>
|
onMouseDown={(e) => {
|
||||||
) : (
|
const element = e.currentTarget as HTMLDivElement;
|
||||||
tableMotion
|
// Focus without scrolling into view
|
||||||
)}
|
if (element.focus) {
|
||||||
|
element.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={mergedContainerRef}
|
||||||
|
tabIndex={0}
|
||||||
|
{...animationProps.fadeIn}
|
||||||
|
transition={{ duration: enableEntranceAnimation ? 0.3 : 0, ease: 'anticipate' }}
|
||||||
|
>
|
||||||
|
<ItemTableListStickyUI
|
||||||
|
calculatedColumnWidths={calculatedColumnWidths}
|
||||||
|
CellComponent={optimizedCellComponent}
|
||||||
|
containerRef={containerRef}
|
||||||
|
data={data}
|
||||||
|
enableHeader={!!enableHeader}
|
||||||
|
enableStickyGroupRows={!!enableStickyGroupRows}
|
||||||
|
enableStickyHeader={!!enableStickyHeader}
|
||||||
|
getRowHeightWrapper={getRowHeightWrapper}
|
||||||
|
groups={groups}
|
||||||
|
headerHeight={headerHeight}
|
||||||
|
internalState={internalState}
|
||||||
|
parsedColumns={parsedColumns}
|
||||||
|
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
||||||
|
pinnedLeftColumnRef={pinnedLeftColumnRef}
|
||||||
|
pinnedRightColumnCount={pinnedRightColumnCount}
|
||||||
|
pinnedRightColumnRef={pinnedRightColumnRef}
|
||||||
|
pinnedRowRef={pinnedRowRef}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
rowRef={rowRef}
|
||||||
|
size={size}
|
||||||
|
stickyHeaderItemProps={stickyHeaderItemProps}
|
||||||
|
totalColumnCount={totalColumnCount}
|
||||||
|
/>
|
||||||
|
<MemoizedVirtualizedTableGrid
|
||||||
|
calculatedColumnWidths={calculatedColumnWidths}
|
||||||
|
CellComponent={optimizedCellComponent}
|
||||||
|
data={data}
|
||||||
|
dataWithGroups={dataWithGroups}
|
||||||
|
enableScrollShadow={enableScrollShadow}
|
||||||
|
getItem={getItem}
|
||||||
|
headerHeight={headerHeight}
|
||||||
|
mergedRowRef={mergedRowRef}
|
||||||
|
onRangeChanged={onRangeChanged}
|
||||||
|
parsedColumns={parsedColumns}
|
||||||
|
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
||||||
|
pinnedLeftColumnRef={pinnedLeftColumnRef}
|
||||||
|
pinnedRightColumnCount={pinnedRightColumnCount}
|
||||||
|
pinnedRightColumnRef={pinnedRightColumnRef}
|
||||||
|
pinnedRowCount={pinnedRowCount}
|
||||||
|
pinnedRowRef={pinnedRowRef}
|
||||||
|
showLeftShadow={showLeftShadow}
|
||||||
|
showRightShadow={showRightShadow}
|
||||||
|
showTopShadow={showTopShadow}
|
||||||
|
tableConfig={tableConfigValue}
|
||||||
|
totalColumnCount={totalColumnCount}
|
||||||
|
totalRowCount={totalRowCount}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
</ItemTableListConfigProvider>
|
</ItemTableListConfigProvider>
|
||||||
</ItemTableListStoreProvider>
|
</ItemTableListStoreProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
export interface TableScrollShadowSnapshot {
|
|
||||||
showLeftShadow: boolean;
|
|
||||||
showRightShadow: boolean;
|
|
||||||
showTopShadow: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TableScrollShadowStore = ReturnType<typeof createTableScrollShadowStore>;
|
|
||||||
|
|
||||||
export function createTableScrollShadowStore() {
|
|
||||||
let snapshot: TableScrollShadowSnapshot = {
|
|
||||||
showLeftShadow: false,
|
|
||||||
showRightShadow: false,
|
|
||||||
showTopShadow: false,
|
|
||||||
};
|
|
||||||
const listeners = new Set<() => void>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
getSnapshot: (): TableScrollShadowSnapshot => snapshot,
|
|
||||||
setSnapshot: (patch: Partial<TableScrollShadowSnapshot>) => {
|
|
||||||
const next: TableScrollShadowSnapshot = { ...snapshot, ...patch };
|
|
||||||
if (
|
|
||||||
next.showLeftShadow === snapshot.showLeftShadow &&
|
|
||||||
next.showRightShadow === snapshot.showRightShadow &&
|
|
||||||
next.showTopShadow === snapshot.showTopShadow
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
snapshot = next;
|
|
||||||
listeners.forEach((l) => l());
|
|
||||||
},
|
|
||||||
subscribe: (listener: () => void) => {
|
|
||||||
listeners.add(listener);
|
|
||||||
return () => listeners.delete(listener);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 var(--theme-spacing-xs);
|
padding: var(--theme-spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-wrapper.hidden {
|
.title-wrapper.hidden {
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ import { useWindowSettings } from '/@/renderer/store/settings.store';
|
|||||||
import { Flex, FlexProps } from '/@/shared/components/flex/flex';
|
import { Flex, FlexProps } from '/@/shared/components/flex/flex';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
export interface PageHeaderProps extends Omit<
|
export interface PageHeaderProps
|
||||||
FlexProps,
|
extends Omit<FlexProps, 'onAnimationStart' | 'onDrag' | 'onDragEnd' | 'onDragStart'> {
|
||||||
'onAnimationStart' | 'onDrag' | 'onDragEnd' | 'onDragStart'
|
|
||||||
> {
|
|
||||||
animated?: boolean;
|
animated?: boolean;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
|||||||
@@ -54,15 +54,15 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
|
|||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
isFilterValueSet(query[FILTER_KEYS.ALBUM._CUSTOM]) ||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM._CUSTOM]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.ALBUM.ARTIST_IDS]) ||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.ARTIST_IDS]) ||
|
||||||
query[FILTER_KEYS.ALBUM.COMPILATION] !== undefined ||
|
query[FILTER_KEYS.ALBUM.COMPILATION] !== undefined ||
|
||||||
query[FILTER_KEYS.ALBUM.FAVORITE] !== undefined ||
|
query[FILTER_KEYS.ALBUM.FAVORITE] !== undefined ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.ALBUM.GENRE_ID]) ||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.GENRE_ID]) ||
|
||||||
query[FILTER_KEYS.ALBUM.HAS_RATING] !== undefined ||
|
query[FILTER_KEYS.ALBUM.HAS_RATING] !== undefined ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.ALBUM.MAX_YEAR]) ||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.MAX_YEAR]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.ALBUM.MIN_YEAR]) ||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.MIN_YEAR]) ||
|
||||||
query[FILTER_KEYS.ALBUM.RECENTLY_PLAYED] !== undefined ||
|
query[FILTER_KEYS.ALBUM.RECENTLY_PLAYED] !== undefined ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SHARED.SEARCH_TERM]),
|
isFilterValueSet(query[FILTER_KEYS.SHARED.SEARCH_TERM]),
|
||||||
);
|
);
|
||||||
}, [albumFilters.query]);
|
}, [albumFilters.query]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
setMultipleSearchParams,
|
setMultipleSearchParams,
|
||||||
setSearchParam,
|
setSearchParam,
|
||||||
} from '/@/renderer/utils/query-params';
|
} from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -75,10 +74,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setGenreId = useCallback(
|
const setGenreId = useCallback(
|
||||||
(value: null | string[]) => {
|
(value: null | string[]) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.GENRE_ID, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.GENRE_ID, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -86,13 +83,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setAlbumArtist = useCallback(
|
const setAlbumArtist = useCallback(
|
||||||
(value: null | string[]) => {
|
(value: null | string[]) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.ARTIST_IDS, value), {
|
||||||
setSearchParams(
|
replace: true,
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.ARTIST_IDS, value),
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -100,10 +92,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setMinYear = useCallback(
|
const setMinYear = useCallback(
|
||||||
(value: null | number) => {
|
(value: null | number) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MIN_YEAR, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MIN_YEAR, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -111,10 +101,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setMaxYear = useCallback(
|
const setMaxYear = useCallback(
|
||||||
(value: null | number) => {
|
(value: null | number) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MAX_YEAR, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.MAX_YEAR, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -122,10 +110,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setFavorite = useCallback(
|
const setFavorite = useCallback(
|
||||||
(value: boolean | null) => {
|
(value: boolean | null) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.FAVORITE, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.FAVORITE, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -133,13 +119,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setCompilation = useCallback(
|
const setCompilation = useCallback(
|
||||||
(value: boolean | null) => {
|
(value: boolean | null) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.COMPILATION, value), {
|
||||||
setSearchParams(
|
replace: true,
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.COMPILATION, value),
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -147,13 +128,8 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setHasRating = useCallback(
|
const setHasRating = useCallback(
|
||||||
(value: boolean | null) => {
|
(value: boolean | null) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.HAS_RATING, value), {
|
||||||
setSearchParams(
|
replace: true,
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.HAS_RATING, value),
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -161,71 +137,65 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
|
|||||||
|
|
||||||
const setRecentlyPlayed = useCallback(
|
const setRecentlyPlayed = useCallback(
|
||||||
(value: boolean | null) => {
|
(value: boolean | null) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.RECENTLY_PLAYED, value),
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.RECENTLY_PLAYED, value),
|
{
|
||||||
{
|
replace: true,
|
||||||
replace: true,
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setCustom = useCallback(
|
const setCustom = useCallback(
|
||||||
(value: null | Record<string, any>) => {
|
(value: null | Record<string, any>) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM);
|
||||||
const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM);
|
|
||||||
|
|
||||||
const newCustom = {
|
const newCustom = {
|
||||||
...(previousValue ? JSON.parse(previousValue) : {}),
|
...(previousValue ? JSON.parse(previousValue) : {}),
|
||||||
...value,
|
...value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredNewCustom = Object.fromEntries(
|
const filteredNewCustom = Object.fromEntries(
|
||||||
Object.entries(newCustom).filter(
|
Object.entries(newCustom).filter(
|
||||||
([, value]) => value !== null && value !== undefined,
|
([, value]) => value !== null && value !== undefined,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom));
|
prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom));
|
||||||
return prev;
|
return prev;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replace: true,
|
replace: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) =>
|
||||||
(prev) =>
|
setMultipleSearchParams(
|
||||||
setMultipleSearchParams(
|
prev,
|
||||||
prev,
|
{
|
||||||
{
|
[FILTER_KEYS.ALBUM._CUSTOM]: null,
|
||||||
[FILTER_KEYS.ALBUM._CUSTOM]: null,
|
[FILTER_KEYS.ALBUM.ARTIST_IDS]: null,
|
||||||
[FILTER_KEYS.ALBUM.ARTIST_IDS]: null,
|
[FILTER_KEYS.ALBUM.COMPILATION]: null,
|
||||||
[FILTER_KEYS.ALBUM.COMPILATION]: null,
|
[FILTER_KEYS.ALBUM.FAVORITE]: null,
|
||||||
[FILTER_KEYS.ALBUM.FAVORITE]: null,
|
[FILTER_KEYS.ALBUM.GENRE_ID]: null,
|
||||||
[FILTER_KEYS.ALBUM.GENRE_ID]: null,
|
[FILTER_KEYS.ALBUM.HAS_RATING]: null,
|
||||||
[FILTER_KEYS.ALBUM.HAS_RATING]: null,
|
[FILTER_KEYS.ALBUM.MAX_YEAR]: null,
|
||||||
[FILTER_KEYS.ALBUM.MAX_YEAR]: null,
|
[FILTER_KEYS.ALBUM.MIN_YEAR]: null,
|
||||||
[FILTER_KEYS.ALBUM.MIN_YEAR]: null,
|
[FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: null,
|
||||||
[FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: null,
|
[FILTER_KEYS.SHARED.SEARCH_TERM]: null,
|
||||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: null,
|
},
|
||||||
},
|
new Set([FILTER_KEYS.ALBUM._CUSTOM]),
|
||||||
new Set([FILTER_KEYS.ALBUM._CUSTOM]),
|
),
|
||||||
),
|
{ replace: true },
|
||||||
{ replace: true },
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [setSearchParams]);
|
}, [setSearchParams]);
|
||||||
|
|
||||||
const query = useMemo(
|
const query = useMemo(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
||||||
@@ -16,7 +16,7 @@ import { LibraryContainer } from '/@/renderer/features/shared/components/library
|
|||||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||||
import { useAlbumBackground, useCurrentServerId } from '/@/renderer/store';
|
import { useAlbumBackground, useCurrentServer } from '/@/renderer/store';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const ALBUM_DETAIL_BG_FALLBACK = 'var(--theme-colors-foreground-muted)';
|
const ALBUM_DETAIL_BG_FALLBACK = 'var(--theme-colors-foreground-muted)';
|
||||||
@@ -27,10 +27,13 @@ const AlbumDetailRoute = () => {
|
|||||||
const { albumBackground, albumBackgroundBlur } = useAlbumBackground();
|
const { albumBackground, albumBackgroundBlur } = useAlbumBackground();
|
||||||
|
|
||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const serverId = useCurrentServerId();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const detailQuery = useSuspenseQuery({
|
const location = useLocation();
|
||||||
...albumQueries.detail({ query: { id: albumId }, serverId }),
|
|
||||||
|
const detailQuery = useQuery({
|
||||||
|
...albumQueries.detail({ query: { id: albumId }, serverId: server?.id }),
|
||||||
|
placeholderData: location.state?.item,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
@@ -62,7 +65,9 @@ const AlbumDetailRoute = () => {
|
|||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
variant="default"
|
variant="default"
|
||||||
/>
|
/>
|
||||||
<LibraryHeaderBar.Title>{detailQuery.data.name}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
|
{detailQuery?.data?.name}
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
),
|
),
|
||||||
offset: 200,
|
offset: 200,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, Link, useParams } from 'react-router';
|
import { generatePath, Link, useParams } from 'react-router';
|
||||||
@@ -39,7 +39,7 @@ const DummyAlbumDetailRoute = () => {
|
|||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const queryKey = queryKeys.songs.detail(server?.id || '', albumId);
|
const queryKey = queryKeys.songs.detail(server?.id || '', albumId);
|
||||||
const detailQuery = useSuspenseQuery({
|
const detailQuery = useQuery({
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
return api.controller.getSongDetail({
|
return api.controller.getSongDetail({
|
||||||
apiClientProps: { serverId: server?.id || '', signal },
|
apiClientProps: { serverId: server?.id || '', signal },
|
||||||
@@ -52,7 +52,7 @@ const DummyAlbumDetailRoute = () => {
|
|||||||
const { background, colorId } = useFastAverageColor({
|
const { background, colorId } = useFastAverageColor({
|
||||||
id: albumId,
|
id: albumId,
|
||||||
src: detailQuery.data?.imageUrl,
|
src: detailQuery.data?.imageUrl,
|
||||||
srcLoaded: Boolean(detailQuery.data?.imageUrl),
|
srcLoaded: !detailQuery.isLoading,
|
||||||
});
|
});
|
||||||
const { addToQueueByFetch } = usePlayer();
|
const { addToQueueByFetch } = usePlayer();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistListInfiniteGridProps extends ItemListGridComponentProps<AlbumArtistListQuery> {}
|
interface AlbumArtistListInfiniteGridProps
|
||||||
|
extends ItemListGridComponentProps<AlbumArtistListQuery> {}
|
||||||
|
|
||||||
export const AlbumArtistListInfiniteGrid = ({
|
export const AlbumArtistListInfiniteGrid = ({
|
||||||
gap = 'md',
|
gap = 'md',
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistListInfiniteTableProps extends ItemListTableComponentProps<AlbumArtistListQuery> {}
|
interface AlbumArtistListInfiniteTableProps
|
||||||
|
extends ItemListTableComponentProps<AlbumArtistListQuery> {}
|
||||||
|
|
||||||
export const AlbumArtistListInfiniteTable = ({
|
export const AlbumArtistListInfiniteTable = ({
|
||||||
autoFitColumns = false,
|
autoFitColumns = false,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistListPaginatedGridProps extends ItemListGridComponentProps<AlbumArtistListQuery> {}
|
interface AlbumArtistListPaginatedGridProps
|
||||||
|
extends ItemListGridComponentProps<AlbumArtistListQuery> {}
|
||||||
|
|
||||||
export const AlbumArtistListPaginatedGrid = ({
|
export const AlbumArtistListPaginatedGrid = ({
|
||||||
gap = 'md',
|
gap = 'md',
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistListPaginatedTableProps extends ItemListTableComponentProps<AlbumArtistListQuery> {}
|
interface AlbumArtistListPaginatedTableProps
|
||||||
|
extends ItemListTableComponentProps<AlbumArtistListQuery> {}
|
||||||
|
|
||||||
export const AlbumArtistListPaginatedTable = ({
|
export const AlbumArtistListPaginatedTable = ({
|
||||||
autoFitColumns = false,
|
autoFitColumns = false,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-f
|
|||||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { setMultipleSearchParams } from '/@/renderer/utils/query-params';
|
import { setMultipleSearchParams } from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { AlbumArtistListSort } from '/@/shared/types/domain-types';
|
import { AlbumArtistListSort } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -20,15 +19,13 @@ export const useAlbumArtistListFilters = () => {
|
|||||||
const [, setSearchParams] = useSearchParams();
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) =>
|
||||||
(prev) =>
|
setMultipleSearchParams(prev, {
|
||||||
setMultipleSearchParams(prev, {
|
[FILTER_KEYS.SHARED.SEARCH_TERM]: null,
|
||||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: null,
|
}),
|
||||||
}),
|
{ replace: true },
|
||||||
{ replace: true },
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [setSearchParams]);
|
}, [setSearchParams]);
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
|
|||||||
+17
-15
@@ -1,4 +1,4 @@
|
|||||||
import { useSuspenseQueries } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
@@ -35,18 +35,20 @@ const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
|||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const pageKey = LibraryItem.SONG;
|
const pageKey = LibraryItem.SONG;
|
||||||
|
|
||||||
const [detailQuery, favoriteSongsQuery] = useSuspenseQueries({
|
const detailQuery = useQuery(
|
||||||
queries: [
|
artistsQueries.albumArtistDetail({
|
||||||
artistsQueries.albumArtistDetail({
|
query: { id: routeId },
|
||||||
query: { id: routeId },
|
serverId: server?.id,
|
||||||
serverId: server?.id,
|
}),
|
||||||
}),
|
);
|
||||||
artistsQueries.favoriteSongs({
|
|
||||||
query: { artistId: routeId },
|
const favoriteSongsQuery = useQuery(
|
||||||
serverId: server?.id,
|
artistsQueries.favoriteSongs({
|
||||||
}),
|
options: { enabled: !!detailQuery?.data?.name },
|
||||||
],
|
query: { artistId: routeId },
|
||||||
});
|
serverId: server?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const songs = useMemo(
|
const songs = useMemo(
|
||||||
() => favoriteSongsQuery?.data?.items || [],
|
() => favoriteSongsQuery?.data?.items || [],
|
||||||
@@ -166,7 +168,7 @@ const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlbumArtistDetailFavoriteSongsListRouteWithBoundary = () => {
|
const AlbumArtistDetailTopSongsListRouteWithBoundary = () => {
|
||||||
return (
|
return (
|
||||||
<PageErrorBoundary>
|
<PageErrorBoundary>
|
||||||
<AlbumArtistDetailFavoriteSongsListRoute />
|
<AlbumArtistDetailFavoriteSongsListRoute />
|
||||||
@@ -174,4 +176,4 @@ const AlbumArtistDetailFavoriteSongsListRouteWithBoundary = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AlbumArtistDetailFavoriteSongsListRouteWithBoundary;
|
export default AlbumArtistDetailTopSongsListRouteWithBoundary;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
@@ -34,15 +34,16 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
key: 'album-artist-top-songs-query-type',
|
key: 'album-artist-top-songs-query-type',
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailQuery = useSuspenseQuery(
|
const detailQuery = useQuery(
|
||||||
artistsQueries.albumArtistDetail({
|
artistsQueries.albumArtistDetail({
|
||||||
query: { id: routeId },
|
query: { id: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const topSongsQuery = useSuspenseQuery(
|
const topSongsQuery = useQuery(
|
||||||
artistsQueries.topSongs({
|
artistsQueries.topSongs({
|
||||||
|
options: { enabled: !!detailQuery?.data?.name },
|
||||||
query: {
|
query: {
|
||||||
artist: detailQuery?.data?.name || '',
|
artist: detailQuery?.data?.name || '',
|
||||||
artistId: routeId,
|
artistId: routeId,
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
|
|||||||
innerProps: {
|
innerProps: {
|
||||||
...modalProps,
|
...modalProps,
|
||||||
},
|
},
|
||||||
modal: 'addToPlaylist',
|
modalKey: 'addToPlaylist',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
title: t('page.contextMenu.addToPlaylist', { postProcess: 'sentenceCase' }),
|
title: t('page.contextMenu.addToPlaylist', { postProcess: 'sentenceCase' }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ShareAction = ({ ids, itemType }: ShareActionProps) => {
|
|||||||
itemIds: ids,
|
itemIds: ids,
|
||||||
resourceType,
|
resourceType,
|
||||||
},
|
},
|
||||||
modal: 'shareItem',
|
modalKey: 'shareItem',
|
||||||
title: t('page.contextMenu.shareItem', { postProcess: 'titleCase' }),
|
title: t('page.contextMenu.shareItem', { postProcess: 'titleCase' }),
|
||||||
});
|
});
|
||||||
}, [ids, resourceType, t]);
|
}, [ids, resourceType, t]);
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ export const PlaylistContextMenu = ({ items, type }: PlaylistContextMenuProps) =
|
|||||||
<ContextMenu.Divider />
|
<ContextMenu.Divider />
|
||||||
<AddToPlaylistAction items={ids} itemType={LibraryItem.PLAYLIST} />
|
<AddToPlaylistAction items={ids} itemType={LibraryItem.PLAYLIST} />
|
||||||
<ContextMenu.Divider />
|
<ContextMenu.Divider />
|
||||||
|
<GetInfoAction disabled={items.length === 0} items={items} />
|
||||||
|
<ContextMenu.Divider />
|
||||||
<EditPlaylistAction disabled={!canEditPlaylist} items={items} />
|
<EditPlaylistAction disabled={!canEditPlaylist} items={items} />
|
||||||
<DeletePlaylistAction disabled={!canDeletePlaylist} items={items} />
|
<DeletePlaylistAction disabled={!canDeletePlaylist} items={items} />
|
||||||
<ContextMenu.Divider />
|
|
||||||
<GetInfoAction disabled={items.length === 0} items={items} />
|
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Content>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-f
|
|||||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { parseJsonParam, setJsonSearchParam } from '/@/renderer/utils/query-params';
|
import { parseJsonParam, setJsonSearchParam } from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -30,19 +29,13 @@ export const useFolderListFilters = () => {
|
|||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const setFolderPath = (path: FolderPathItem[]) => {
|
const setFolderPath = (path: FolderPathItem[]) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const newParams = setJsonSearchParam(prev, FILTER_KEYS.FOLDER.FOLDER_PATH, path);
|
||||||
const newParams = setJsonSearchParam(
|
return newParams;
|
||||||
prev,
|
},
|
||||||
FILTER_KEYS.FOLDER.FOLDER_PATH,
|
{ replace: false },
|
||||||
path,
|
);
|
||||||
);
|
|
||||||
return newParams;
|
|
||||||
},
|
|
||||||
{ replace: false },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate to a folder (adds to path)
|
// Navigate to a folder (adds to path)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import i18n from '/@/i18n/i18n';
|
|||||||
export const openLyricsSettingsModal = (settingsKey: string = 'default') => {
|
export const openLyricsSettingsModal = (settingsKey: string = 'default') => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: { settingsKey },
|
innerProps: { settingsKey },
|
||||||
modal: 'lyricsSettings',
|
modalKey: 'lyricsSettings',
|
||||||
overlayProps: {
|
overlayProps: {
|
||||||
blur: 0,
|
blur: 0,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
.toolbar {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
container-type: inline-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.restore-section {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 296px) {
|
|
||||||
.restore-section {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3,8 +3,6 @@ import { t } from 'i18next';
|
|||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import styles from './play-queue-list-controls.module.css';
|
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||||
@@ -18,8 +16,6 @@ import { SearchInput } from '/@/renderer/features/shared/components/search-input
|
|||||||
import { useCurrentServer, usePlayerStoreBase } from '/@/renderer/store';
|
import { useCurrentServer, usePlayerStoreBase } from '/@/renderer/store';
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||||
@@ -37,53 +33,6 @@ export const PlayQueueListControls = ({
|
|||||||
tableRef,
|
tableRef,
|
||||||
type,
|
type,
|
||||||
}: PlayQueueListOptionsProps) => {
|
}: PlayQueueListOptionsProps) => {
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
align="center"
|
|
||||||
className={styles.toolbar}
|
|
||||||
gap="sm"
|
|
||||||
justify="flex-start"
|
|
||||||
px="md"
|
|
||||||
py="xs"
|
|
||||||
style={{ borderBottom: '1px solid var(--theme-colors-border)' }}
|
|
||||||
w="100%"
|
|
||||||
wrap="nowrap"
|
|
||||||
>
|
|
||||||
<Group gap="xs" style={{ flexShrink: 0 }} wrap="nowrap">
|
|
||||||
<QueueRestoreActions />
|
|
||||||
<QueuePlaybackIcons tableRef={tableRef} />
|
|
||||||
</Group>
|
|
||||||
<Divider h="60%" orientation="vertical" style={{ alignSelf: 'center' }} />
|
|
||||||
<Box style={{ display: 'flex', flex: 1, minWidth: 0 }}>
|
|
||||||
<SearchInput
|
|
||||||
enableHotkey={false}
|
|
||||||
fillContainer
|
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
|
||||||
value={searchTerm}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Divider h="60%" orientation="vertical" style={{ alignSelf: 'center' }} />
|
|
||||||
<Box style={{ flexShrink: 0 }}>
|
|
||||||
<ListConfigMenu
|
|
||||||
displayTypes={[
|
|
||||||
{ hidden: true, value: ListDisplayType.GRID },
|
|
||||||
...SONG_DISPLAY_TYPES,
|
|
||||||
]}
|
|
||||||
listKey={type}
|
|
||||||
optionsConfig={{
|
|
||||||
table: {
|
|
||||||
itemsPerPage: { hidden: true },
|
|
||||||
pagination: { hidden: true },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
tableColumnsData={SONG_TABLE_COLUMNS}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const QueuePlaybackIcons = ({ tableRef }: { tableRef: RefObject<ItemListHandle | null> }) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
|
|
||||||
@@ -103,29 +52,53 @@ const QueuePlaybackIcons = ({ tableRef }: { tableRef: RefObject<ItemListHandle |
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Group h="65px" justify="space-between" px="1rem" py="1rem" w="100%">
|
||||||
<ActionIcon
|
<Group gap="xs">
|
||||||
icon="mediaShuffle"
|
<QueueRestoreActions />
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handleShuffleQueue}
|
icon="mediaShuffle"
|
||||||
tooltip={{ label: t('player.shuffle', { postProcess: 'sentenceCase' }) }}
|
iconProps={{ size: 'lg' }}
|
||||||
variant="subtle"
|
onClick={handleShuffleQueue}
|
||||||
/>
|
tooltip={{ label: t('player.shuffle', { postProcess: 'sentenceCase' }) }}
|
||||||
<ActionIcon
|
variant="subtle"
|
||||||
icon="x"
|
/>
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handleClearQueue}
|
icon="x"
|
||||||
tooltip={{ label: t('action.clearQueue', { postProcess: 'sentenceCase' }) }}
|
iconProps={{ size: 'lg' }}
|
||||||
variant="subtle"
|
onClick={handleClearQueue}
|
||||||
/>
|
tooltip={{ label: t('action.clearQueue', { postProcess: 'sentenceCase' }) }}
|
||||||
<ActionIcon
|
variant="subtle"
|
||||||
icon="goToItem"
|
/>
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handleJumpToCurrent}
|
icon="goToItem"
|
||||||
tooltip={{ label: t('action.goToCurrent', { postProcess: 'sentenceCase' }) }}
|
iconProps={{ size: 'lg' }}
|
||||||
variant="subtle"
|
onClick={handleJumpToCurrent}
|
||||||
/>
|
tooltip={{ label: t('action.goToCurrent', { postProcess: 'sentenceCase' }) }}
|
||||||
</>
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<SearchInput
|
||||||
|
enableHotkey={false}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
value={searchTerm}
|
||||||
|
/>
|
||||||
|
<ListConfigMenu
|
||||||
|
displayTypes={[
|
||||||
|
{ hidden: true, value: ListDisplayType.GRID },
|
||||||
|
...SONG_DISPLAY_TYPES,
|
||||||
|
]}
|
||||||
|
listKey={type}
|
||||||
|
optionsConfig={{
|
||||||
|
table: {
|
||||||
|
itemsPerPage: { hidden: true },
|
||||||
|
pagination: { hidden: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
tableColumnsData={SONG_TABLE_COLUMNS}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,7 +117,7 @@ const QueueRestoreActions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.restoreSection}>
|
<>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
disabled={Boolean(isFetching)}
|
disabled={Boolean(isFetching)}
|
||||||
icon="upload"
|
icon="upload"
|
||||||
@@ -171,6 +144,6 @@ const QueueRestoreActions = () => {
|
|||||||
}}
|
}}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
</span>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { ItemListKey, Platform } from '/@/shared/types/types';
|
import { ItemListKey, Platform, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
type SidebarPanelType = 'lyrics' | 'queue' | 'visualizer';
|
type SidebarPanelType = 'lyrics' | 'queue' | 'visualizer';
|
||||||
|
|
||||||
@@ -55,9 +55,9 @@ export const SidebarPlayQueue = () => {
|
|||||||
const showLyricsInSidebar = useShowLyricsInSidebar();
|
const showLyricsInSidebar = useShowLyricsInSidebar();
|
||||||
const showVisualizerInSidebar = useShowVisualizerInSidebar();
|
const showVisualizerInSidebar = useShowVisualizerInSidebar();
|
||||||
const sidebarPanelOrder = useSidebarPanelOrder();
|
const sidebarPanelOrder = useSidebarPanelOrder();
|
||||||
const { webAudio } = usePlaybackSettings();
|
const { type, webAudio } = usePlaybackSettings();
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const showVisualizer = showVisualizerInSidebar && webAudio;
|
const showVisualizer = showVisualizerInSidebar && type === PlayerType.WEB && webAudio;
|
||||||
const showPanel = showLyricsInSidebar || showVisualizer;
|
const showPanel = showLyricsInSidebar || showVisualizer;
|
||||||
|
|
||||||
const shouldAddTopMargin = isElectron() && windowBarStyle === Platform.WEB;
|
const shouldAddTopMargin = isElectron() && windowBarStyle === Platform.WEB;
|
||||||
@@ -374,8 +374,8 @@ const CombinedLyricsAndVisualizerPanel = () => {
|
|||||||
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
||||||
const showLyricsInSidebar = useShowLyricsInSidebar();
|
const showLyricsInSidebar = useShowLyricsInSidebar();
|
||||||
const showVisualizerInSidebar = useShowVisualizerInSidebar();
|
const showVisualizerInSidebar = useShowVisualizerInSidebar();
|
||||||
const { webAudio } = usePlaybackSettings();
|
const { type, webAudio } = usePlaybackSettings();
|
||||||
const showVisualizer = showVisualizerInSidebar && webAudio;
|
const showVisualizer = showVisualizerInSidebar && type === PlayerType.WEB && webAudio;
|
||||||
|
|
||||||
const { data: lyricsData } = useQuery(
|
const { data: lyricsData } = useQuery(
|
||||||
lyricsQueries.songLyrics(
|
lyricsQueries.songLyrics(
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
useIsRadioActive,
|
useIsRadioActive,
|
||||||
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||||
import { RemoteHook } from '/@/renderer/features/remote/hooks/use-remote';
|
import { RemoteHook } from '/@/renderer/features/remote/hooks/use-remote';
|
||||||
import { VisualizerSystemAudioBridgeHook } from '/@/renderer/features/visualizer/components/visualizer-system-audio-bridge';
|
|
||||||
import {
|
import {
|
||||||
updateQueueFavorites,
|
updateQueueFavorites,
|
||||||
updateQueueRatings,
|
updateQueueRatings,
|
||||||
@@ -41,21 +40,11 @@ import { PlayerType } from '/@/shared/types/types';
|
|||||||
|
|
||||||
const CODEC_PROBES = [
|
const CODEC_PROBES = [
|
||||||
{ codec: 'mp3', container: 'mp3', mime: 'audio/mpeg' },
|
{ codec: 'mp3', container: 'mp3', mime: 'audio/mpeg' },
|
||||||
|
|
||||||
{ codec: 'aac', container: 'mp4', mime: 'audio/mp4; codecs="mp4a.40.2"' },
|
{ codec: 'aac', container: 'mp4', mime: 'audio/mp4; codecs="mp4a.40.2"' },
|
||||||
{ codec: 'aac', container: 'aac', mime: 'audio/aac' },
|
|
||||||
{ codec: 'aac', container: 'mp4', mime: 'audio/x-m4a' },
|
|
||||||
|
|
||||||
{ codec: 'opus', container: 'ogg', mime: 'audio/ogg; codecs="opus"' },
|
{ codec: 'opus', container: 'ogg', mime: 'audio/ogg; codecs="opus"' },
|
||||||
{ codec: 'opus', container: 'webm', mime: 'audio/webm; codecs="opus"' },
|
|
||||||
|
|
||||||
{ codec: 'vorbis', container: 'ogg', mime: 'audio/ogg; codecs="vorbis"' },
|
{ codec: 'vorbis', container: 'ogg', mime: 'audio/ogg; codecs="vorbis"' },
|
||||||
{ codec: 'vorbis', container: 'webm', mime: 'audio/webm; codecs="vorbis"' },
|
|
||||||
|
|
||||||
{ codec: 'flac', container: 'flac', mime: 'audio/flac' },
|
{ codec: 'flac', container: 'flac', mime: 'audio/flac' },
|
||||||
|
{ codec: 'wav', container: 'wav', mime: 'audio/wav' },
|
||||||
{ codec: ['pcm', 'wav'], container: 'wav', mime: 'audio/wav' },
|
|
||||||
|
|
||||||
{ codec: 'alac', container: 'mp4', mime: 'audio/mp4; codecs="alac"' },
|
{ codec: 'alac', container: 'mp4', mime: 'audio/mp4; codecs="alac"' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -64,8 +53,6 @@ const DEFAULT_TRANSCODING_PROFILES = [
|
|||||||
{ audioCodec: 'mp3', container: 'mp3', protocol: 'http' },
|
{ audioCodec: 'mp3', container: 'mp3', protocol: 'http' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SAFARI_TRANSCODING_PROFILES = [{ audioCodec: 'mp3', container: 'mp3', protocol: 'http' }];
|
|
||||||
|
|
||||||
const DIRECT_PLAY_PROFILES: {
|
const DIRECT_PLAY_PROFILES: {
|
||||||
audioCodecs: string[];
|
audioCodecs: string[];
|
||||||
containers: string[];
|
containers: string[];
|
||||||
@@ -73,7 +60,7 @@ const DIRECT_PLAY_PROFILES: {
|
|||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
export function getDefaultTranscodingProfiles() {
|
export function getDefaultTranscodingProfiles() {
|
||||||
return isSafari() ? SAFARI_TRANSCODING_PROFILES : DEFAULT_TRANSCODING_PROFILES;
|
return DEFAULT_TRANSCODING_PROFILES;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDirectPlayProfiles() {
|
export function getDirectPlayProfiles() {
|
||||||
@@ -85,9 +72,9 @@ function detectBrowserProfile() {
|
|||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
|
|
||||||
for (const { codec, container, mime } of CODEC_PROBES) {
|
for (const { codec, container, mime } of CODEC_PROBES) {
|
||||||
if (audio.canPlayType(mime) === 'maybe' || audio.canPlayType(mime) === 'probably') {
|
if (audio.canPlayType(mime) === 'probably') {
|
||||||
DIRECT_PLAY_PROFILES.push({
|
DIRECT_PLAY_PROFILES.push({
|
||||||
audioCodecs: Array.isArray(codec) ? codec : [codec],
|
audioCodecs: [codec],
|
||||||
containers: [container],
|
containers: [container],
|
||||||
protocols: ['http'],
|
protocols: ['http'],
|
||||||
});
|
});
|
||||||
@@ -99,11 +86,6 @@ function detectBrowserProfile() {
|
|||||||
return DIRECT_PLAY_PROFILES;
|
return DIRECT_PLAY_PROFILES;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafari() {
|
|
||||||
const ua = navigator.userAgent;
|
|
||||||
return ua.includes('Safari') && !ua.includes('Chrome') && !ua.includes('Chromium');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AudioPlayers = () => {
|
export const AudioPlayers = () => {
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
@@ -136,7 +118,6 @@ export const AudioPlayers = () => {
|
|||||||
<UpdateCurrentSongHook />
|
<UpdateCurrentSongHook />
|
||||||
<RadioAudioInstanceHook />
|
<RadioAudioInstanceHook />
|
||||||
<RadioMetadataHook />
|
<RadioMetadataHook />
|
||||||
<VisualizerSystemAudioBridgeHook />
|
|
||||||
<AutosaveHook />
|
<AutosaveHook />
|
||||||
<AudioPlayersContent
|
<AudioPlayersContent
|
||||||
audioContext={audioContext}
|
audioContext={audioContext}
|
||||||
|
|||||||
@@ -269,7 +269,25 @@ export const FullScreenPlayerImage = () => {
|
|||||||
? radioMetadata?.title || stationName || 'Radio'
|
? radioMetadata?.title || stationName || 'Radio'
|
||||||
: currentSong?.name}
|
: currentSong?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text key="fs-artists" size="xl">
|
{isPlayingRadio ? (
|
||||||
|
<Text overflow="hidden" size="xl" w="100%">
|
||||||
|
{stationName || 'Radio'}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
component={Link}
|
||||||
|
isLink
|
||||||
|
overflow="hidden"
|
||||||
|
size="xl"
|
||||||
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
|
albumId: currentSong?.albumId || '',
|
||||||
|
})}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
{currentSong?.album}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text key="fs-artists">
|
||||||
{isPlayingRadio
|
{isPlayingRadio
|
||||||
? radioMetadata?.artist || stationName || 'Radio'
|
? radioMetadata?.artist || stationName || 'Radio'
|
||||||
: currentSong?.artists?.map((artist, index) => (
|
: currentSong?.artists?.map((artist, index) => (
|
||||||
@@ -296,24 +314,6 @@ export const FullScreenPlayerImage = () => {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
{isPlayingRadio ? (
|
|
||||||
<Text overflow="hidden" size="xl" w="100%">
|
|
||||||
{stationName || 'Radio'}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
component={Link}
|
|
||||||
isLink
|
|
||||||
overflow="hidden"
|
|
||||||
size="xl"
|
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
|
||||||
albumId: currentSong?.albumId || '',
|
|
||||||
})}
|
|
||||||
w="100%"
|
|
||||||
>
|
|
||||||
{currentSong?.album}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{!isPlayingRadio && (
|
{!isPlayingRadio && (
|
||||||
<Group justify="center" mt="sm">
|
<Group justify="center" mt="sm">
|
||||||
{playerItems.map((i) => !i.disabled && builtDataItems[i.id])}
|
{playerItems.map((i) => !i.disabled && builtDataItems[i.id])}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '/@/renderer/store/full-screen-player.store';
|
} from '/@/renderer/store/full-screen-player.store';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
||||||
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
|
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
|
||||||
@@ -33,7 +33,7 @@ export const FullScreenPlayerQueue = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { activeTab, opacity } = useFullScreenPlayerStore();
|
const { activeTab, opacity } = useFullScreenPlayerStore();
|
||||||
const { setStore } = useFullScreenPlayerStoreActions();
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
const { webAudio } = usePlaybackSettings();
|
const { type, webAudio } = usePlaybackSettings();
|
||||||
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
||||||
|
|
||||||
const headerItems = useMemo(() => {
|
const headerItems = useMemo(() => {
|
||||||
@@ -55,7 +55,7 @@ export const FullScreenPlayerQueue = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (webAudio) {
|
if (type === PlayerType.WEB && webAudio) {
|
||||||
items.push({
|
items.push({
|
||||||
active: activeTab === 'visualizer',
|
active: activeTab === 'visualizer',
|
||||||
label: t('page.fullscreenPlayer.visualizer', { postProcess: 'titleCase' }),
|
label: t('page.fullscreenPlayer.visualizer', { postProcess: 'titleCase' }),
|
||||||
@@ -64,7 +64,7 @@ export const FullScreenPlayerQueue = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [activeTab, setStore, t, webAudio]);
|
}, [activeTab, setStore, t, type, webAudio]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -119,7 +119,7 @@ export const FullScreenPlayerQueue = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : activeTab === 'lyrics' ? (
|
) : activeTab === 'lyrics' ? (
|
||||||
<Lyrics fadeOutNoLyricsMessage={false} />
|
<Lyrics fadeOutNoLyricsMessage={false} />
|
||||||
) : activeTab === 'visualizer' && webAudio ? (
|
) : activeTab === 'visualizer' && type === PlayerType.WEB && webAudio ? (
|
||||||
<Suspense fallback={<></>}>
|
<Suspense fallback={<></>}>
|
||||||
{visualizerType === 'butterchurn' ? (
|
{visualizerType === 'butterchurn' ? (
|
||||||
<ButterchurnVisualizer />
|
<ButterchurnVisualizer />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
useWindowSettings,
|
useWindowSettings,
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
||||||
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
|
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
|
||||||
@@ -131,7 +131,7 @@ VisualizerContainer.displayName = 'VisualizerContainer';
|
|||||||
export const FullScreenVisualizer = () => {
|
export const FullScreenVisualizer = () => {
|
||||||
const { setStore } = useFullScreenPlayerStoreActions();
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const { webAudio } = usePlaybackSettings();
|
const { type, webAudio } = usePlaybackSettings();
|
||||||
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export const FullScreenVisualizer = () => {
|
|||||||
return (
|
return (
|
||||||
<VisualizerContainer isMobile={isMobile} windowBarStyle={windowBarStyle}>
|
<VisualizerContainer isMobile={isMobile} windowBarStyle={windowBarStyle}>
|
||||||
<div className={styles.visualizerContainer}>
|
<div className={styles.visualizerContainer}>
|
||||||
{webAudio ? (
|
{type === PlayerType.WEB && webAudio ? (
|
||||||
<Suspense fallback={<></>}>
|
<Suspense fallback={<></>}>
|
||||||
{visualizerType === 'butterchurn' ? (
|
{visualizerType === 'butterchurn' ? (
|
||||||
<ButterchurnVisualizer />
|
<ButterchurnVisualizer />
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import { shallow } from 'zustand/shallow';
|
|||||||
import styles from './left-controls.module.css';
|
import styles from './left-controls.module.css';
|
||||||
|
|
||||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import {
|
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
||||||
JOINED_ARTISTS_MUTED_PROPS,
|
|
||||||
JoinedArtists,
|
|
||||||
} from '/@/renderer/features/albums/components/joined-artists';
|
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { RadioMetadataDisplay } from '/@/renderer/features/player/components/radio-metadata-display';
|
import { RadioMetadataDisplay } from '/@/renderer/features/player/components/radio-metadata-display';
|
||||||
import {
|
import {
|
||||||
@@ -269,14 +266,6 @@ export const LeftControls = () => {
|
|||||||
<JoinedArtists
|
<JoinedArtists
|
||||||
artistName={currentSong?.artistName || ''}
|
artistName={currentSong?.artistName || ''}
|
||||||
artists={artists || []}
|
artists={artists || []}
|
||||||
linkProps={{
|
|
||||||
...JOINED_ARTISTS_MUTED_PROPS.linkProps,
|
|
||||||
size: 'md',
|
|
||||||
}}
|
|
||||||
rootTextProps={{
|
|
||||||
...JOINED_ARTISTS_MUTED_PROPS.rootTextProps,
|
|
||||||
size: 'md',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-top: 1px solid alpha(var(--theme-colors-border), 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-grid {
|
.controls-grid {
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const randomFetchQuery = (args: {
|
|||||||
export const openShuffleAllModal = async () => {
|
export const openShuffleAllModal = async () => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: {},
|
innerProps: {},
|
||||||
modal: 'shuffleAll',
|
modalKey: 'shuffleAll',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
title: i18n.t('player.playRandom', { postProcess: 'sentenceCase' }) as string,
|
title: i18n.t('player.playRandom', { postProcess: 'sentenceCase' }) as string,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import {
|
|
||||||
useFullScreenPlayerStore,
|
|
||||||
usePlaybackSettings,
|
|
||||||
useShowVisualizerInSidebar,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
|
|
||||||
export function useIsLocalVisualizerSurfaceVisible(): boolean {
|
|
||||||
const { webAudio: webAudioEnabled } = usePlaybackSettings();
|
|
||||||
const showVisualizerInSidebar = useShowVisualizerInSidebar();
|
|
||||||
const { activeTab, expanded, visualizerExpanded } = useFullScreenPlayerStore();
|
|
||||||
|
|
||||||
const sidebarVisualizer = showVisualizerInSidebar && webAudioEnabled;
|
|
||||||
const fullScreenPlayerVisualizerTab = expanded && activeTab === 'visualizer' && webAudioEnabled;
|
|
||||||
const fullScreenVisualizerOverlay = visualizerExpanded && webAudioEnabled;
|
|
||||||
|
|
||||||
return sidebarVisualizer || fullScreenPlayerVisualizerTab || fullScreenVisualizerOverlay;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { debounce } from 'lodash';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
@@ -10,9 +9,8 @@ import {
|
|||||||
useRadioPlayer,
|
useRadioPlayer,
|
||||||
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||||
import {
|
import {
|
||||||
subscribeCurrentTrack,
|
|
||||||
subscribePlayerStatus,
|
|
||||||
usePlaybackSettings,
|
usePlaybackSettings,
|
||||||
|
usePlaybackType,
|
||||||
usePlayerStore,
|
usePlayerStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useSkipButtons,
|
useSkipButtons,
|
||||||
@@ -31,40 +29,6 @@ export const useMediaSession = () => {
|
|||||||
const isRadioActive = useIsRadioActive();
|
const isRadioActive = useIsRadioActive();
|
||||||
const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer();
|
const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer();
|
||||||
|
|
||||||
// Keep refs to current values to avoid dependency changes triggering handler re-registration
|
|
||||||
const playerRef = useRef(player);
|
|
||||||
const skipRef = useRef(skip);
|
|
||||||
const isRadioActiveRef = useRef(isRadioActive);
|
|
||||||
const isRadioPlayingRef = useRef(isRadioPlaying);
|
|
||||||
const radioMetadataRef = useRef(radioMetadata);
|
|
||||||
const stationNameRef = useRef(stationName);
|
|
||||||
const isMediaSessionEnabledRef = useRef(false);
|
|
||||||
|
|
||||||
// Update refs whenever values change, but don't trigger effects
|
|
||||||
useEffect(() => {
|
|
||||||
playerRef.current = player;
|
|
||||||
}, [player]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
skipRef.current = skip;
|
|
||||||
}, [skip]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isRadioActiveRef.current = isRadioActive;
|
|
||||||
}, [isRadioActive]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isRadioPlayingRef.current = isRadioPlaying;
|
|
||||||
}, [isRadioPlaying]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
radioMetadataRef.current = radioMetadata;
|
|
||||||
}, [radioMetadata]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
stationNameRef.current = stationName;
|
|
||||||
}, [stationName]);
|
|
||||||
|
|
||||||
const isMediaSessionEnabled = useMemo(() => {
|
const isMediaSessionEnabled = useMemo(() => {
|
||||||
// Always enable media session on web
|
// Always enable media session on web
|
||||||
if (!isElectron()) {
|
if (!isElectron()) {
|
||||||
@@ -74,87 +38,71 @@ export const useMediaSession = () => {
|
|||||||
return Boolean(mediaSessionEnabled && playbackType === PlayerType.WEB);
|
return Boolean(mediaSessionEnabled && playbackType === PlayerType.WEB);
|
||||||
}, [mediaSessionEnabled, playbackType]);
|
}, [mediaSessionEnabled, playbackType]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isMediaSessionEnabledRef.current = isMediaSessionEnabled;
|
|
||||||
}, [isMediaSessionEnabled]);
|
|
||||||
|
|
||||||
// Register/unregister handlers whenever isMediaSessionEnabled changes so that
|
|
||||||
// enabling the setting after mount correctly registers handlers instead of
|
|
||||||
// silently no-oping because the [] effect already ran.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMediaSessionEnabled) {
|
if (!isMediaSessionEnabled) {
|
||||||
mediaSession.setActionHandler('nexttrack', null);
|
|
||||||
mediaSession.setActionHandler('pause', null);
|
|
||||||
mediaSession.setActionHandler('play', null);
|
|
||||||
mediaSession.setActionHandler('previoustrack', null);
|
|
||||||
mediaSession.setActionHandler('seekto', null);
|
|
||||||
mediaSession.setActionHandler('stop', null);
|
|
||||||
mediaSession.setActionHandler('seekbackward', null);
|
|
||||||
mediaSession.setActionHandler('seekforward', null);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSession.setActionHandler('nexttrack', () => {
|
mediaSession.setActionHandler('nexttrack', () => {
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.mediaNext();
|
player.mediaNext();
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('pause', () => {
|
mediaSession.setActionHandler('pause', () => {
|
||||||
playerRef.current.mediaPause();
|
player.mediaPause();
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('play', () => {
|
mediaSession.setActionHandler('play', () => {
|
||||||
playerRef.current.mediaPlay();
|
player.mediaPlay();
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('previoustrack', () => {
|
mediaSession.setActionHandler('previoustrack', () => {
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.mediaPrevious();
|
player.mediaPrevious();
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('seekto', (e) => {
|
mediaSession.setActionHandler('seekto', (e) => {
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.seekTime) {
|
if (e.seekTime) {
|
||||||
playerRef.current.mediaSeekToTimestamp(e.seekTime);
|
player.mediaSeekToTimestamp(e.seekTime);
|
||||||
} else if (e.seekOffset) {
|
} else if (e.seekOffset) {
|
||||||
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
||||||
playerRef.current.mediaSeekToTimestamp(currentTimestamp + e.seekOffset);
|
player.mediaSeekToTimestamp(currentTimestamp + e.seekOffset);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('stop', () => {
|
mediaSession.setActionHandler('stop', () => {
|
||||||
playerRef.current.mediaStop();
|
player.mediaStop();
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('seekbackward', (e) => {
|
mediaSession.setActionHandler('seekbackward', (e) => {
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
||||||
playerRef.current.mediaSeekToTimestamp(
|
player.mediaSeekToTimestamp(
|
||||||
currentTimestamp - (e.seekOffset || skipRef.current?.skipBackwardSeconds || 5),
|
currentTimestamp - (e.seekOffset || skip?.skipBackwardSeconds || 5),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('seekforward', (e) => {
|
mediaSession.setActionHandler('seekforward', (e) => {
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
||||||
playerRef.current.mediaSeekToTimestamp(
|
player.mediaSeekToTimestamp(
|
||||||
currentTimestamp + (e.seekOffset || skipRef.current?.skipForwardSeconds || 5),
|
currentTimestamp + (e.seekOffset || skip?.skipForwardSeconds || 5),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,22 +116,28 @@ export const useMediaSession = () => {
|
|||||||
mediaSession.setActionHandler('seekbackward', null);
|
mediaSession.setActionHandler('seekbackward', null);
|
||||||
mediaSession.setActionHandler('seekforward', null);
|
mediaSession.setActionHandler('seekforward', null);
|
||||||
};
|
};
|
||||||
}, [isMediaSessionEnabled]);
|
}, [
|
||||||
|
player,
|
||||||
|
skip?.skipBackwardSeconds,
|
||||||
|
skip?.skipForwardSeconds,
|
||||||
|
isMediaSessionEnabled,
|
||||||
|
isRadioActive,
|
||||||
|
isRadioPlaying,
|
||||||
|
]);
|
||||||
|
|
||||||
const updateMediaSessionMetadata = useCallback(
|
const updateMediaSessionMetadata = useCallback(
|
||||||
(song: QueueSong | undefined) => {
|
(song: QueueSong | undefined) => {
|
||||||
// Read from ref so this callback is never stale regardless of when it was created
|
if (!isMediaSessionEnabled) {
|
||||||
if (!isMediaSessionEnabledRef.current) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle radio metadata when radio is active and playing
|
// Handle radio metadata when radio is active and playing
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
const title = radioMetadataRef.current?.title || stationNameRef.current || 'Radio';
|
const title = radioMetadata?.title || stationName || 'Radio';
|
||||||
const artist = radioMetadataRef.current?.artist || stationNameRef.current || '';
|
const artist = radioMetadata?.artist || stationName || '';
|
||||||
|
|
||||||
mediaSession.metadata = new MediaMetadata({
|
mediaSession.metadata = new MediaMetadata({
|
||||||
album: stationNameRef.current || '',
|
album: stationName || '',
|
||||||
artist: artist,
|
artist: artist,
|
||||||
artwork: [],
|
artwork: [],
|
||||||
title: title,
|
title: title,
|
||||||
@@ -210,88 +164,62 @@ export const useMediaSession = () => {
|
|||||||
title: song?.name ?? '',
|
title: song?.name ?? '',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// All values are read from refs — stable callback, no stale closure risk
|
[isMediaSessionEnabled, isRadioActive, isRadioPlaying, radioMetadata, stationName],
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Debounced version to handle rapid skipping — only the last skip in a burst commits
|
|
||||||
// to the media session. Without this, rapid MediaMetadata assignments can tear the
|
|
||||||
// browser's media session state and permanently drop the handlers.
|
|
||||||
const debouncedUpdateMetadata = useRef(
|
|
||||||
debounce((song: QueueSong | undefined) => {
|
|
||||||
updateMediaSessionMetadata(song);
|
|
||||||
}, 100),
|
|
||||||
).current;
|
|
||||||
|
|
||||||
// Cancel any pending debounced update on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
debouncedUpdateMetadata.cancel();
|
|
||||||
};
|
|
||||||
}, [debouncedUpdateMetadata]);
|
|
||||||
|
|
||||||
// Update metadata when radio metadata changes
|
// Update metadata when radio metadata changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMediaSessionEnabled) {
|
if (!isMediaSessionEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
debouncedUpdateMetadata(undefined);
|
updateMediaSessionMetadata(undefined);
|
||||||
}
|
}
|
||||||
}, [radioMetadata, isRadioPlaying, isMediaSessionEnabled, debouncedUpdateMetadata]);
|
}, [
|
||||||
|
isMediaSessionEnabled,
|
||||||
|
isRadioActive,
|
||||||
|
isRadioPlaying,
|
||||||
|
radioMetadata,
|
||||||
|
stationName,
|
||||||
|
updateMediaSessionMetadata,
|
||||||
|
]);
|
||||||
|
|
||||||
// Subscribe directly to the player store instead of using usePlayerEvents.
|
|
||||||
// usePlayerEvents receives inline handler objects that cause it to re-subscribe on every
|
|
||||||
// render, which destroys and recreates the media session on play/pause and track changes.
|
|
||||||
// subscribeCurrentTrack and subscribePlayerStatus are stable Zustand subscriptions with
|
|
||||||
// proper equality checks — registered once on mount and never torn down mid-session.
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribeCurrentSong = subscribeCurrentTrack(({ song }) => {
|
|
||||||
if (!isMediaSessionEnabledRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debouncedUpdateMetadata(song);
|
|
||||||
});
|
|
||||||
|
|
||||||
const unsubscribeStatus = subscribePlayerStatus(({ status }) => {
|
|
||||||
if (!isMediaSessionEnabledRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSession.playbackState = status === PlayerStatus.PLAYING ? 'playing' : 'paused';
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribeCurrentSong();
|
|
||||||
unsubscribeStatus();
|
|
||||||
};
|
|
||||||
}, [debouncedUpdateMetadata]);
|
|
||||||
|
|
||||||
// onPlayerRepeated fires via eventEmitter (not Zustand), so usePlayerEvents is safe here —
|
|
||||||
// the event emitter uses stable function references for on/off and does not re-subscribe
|
|
||||||
// on render. The inline object is fine because deps is [] and the effect only runs once.
|
|
||||||
usePlayerEvents(
|
usePlayerEvents(
|
||||||
{
|
{
|
||||||
onPlayerRepeated: () => {
|
onCurrentSongChange: (properties) => {
|
||||||
if (!isMediaSessionEnabledRef.current) {
|
if (!isMediaSessionEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRadioActiveRef.current && isRadioPlayingRef.current) {
|
if (isRadioActive && isRadioPlaying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMediaSessionMetadata(properties.song);
|
||||||
|
},
|
||||||
|
onPlayerRepeated: () => {
|
||||||
|
if (!isMediaSessionEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRadioActive && isRadioPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSong = usePlayerStore.getState().getCurrentSong();
|
const currentSong = usePlayerStore.getState().getCurrentSong();
|
||||||
debouncedUpdateMetadata(currentSong);
|
updateMediaSessionMetadata(currentSong);
|
||||||
|
},
|
||||||
|
onPlayerStatus: (properties) => {
|
||||||
|
if (!isMediaSessionEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = properties.status;
|
||||||
|
mediaSession.playbackState = status === PlayerStatus.PLAYING ? 'playing' : 'paused';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[],
|
[isMediaSessionEnabled, isRadioActive, isRadioPlaying, updateMediaSessionMetadata],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +229,18 @@ const MediaSessionHookInner = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MediaSessionHook = () => {
|
export const MediaSessionHook = () => {
|
||||||
// Always render the hook — let the internal guard logic decide whether to act.
|
const isElectronEnv = isElectron();
|
||||||
// Conditional rendering here causes unmount/remount cycles that destroy handlers mid-session.
|
const playbackType = usePlaybackType();
|
||||||
|
const isMediaSessionEnabled = useSettingsStore((state) => state.playback.mediaSession);
|
||||||
|
|
||||||
|
// We always want to enable media session when on web
|
||||||
|
// Otherwise, only enable if it is explicitly enabled in the settings AND using the web player
|
||||||
|
const shouldUseMediaSession =
|
||||||
|
!isElectronEnv || (isMediaSessionEnabled && playbackType === PlayerType.WEB);
|
||||||
|
|
||||||
|
if (!shouldUseMediaSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return React.createElement(MediaSessionHookInner);
|
return React.createElement(MediaSessionHookInner);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import isElectron from 'is-electron';
|
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
|
||||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
|
||||||
import { PlayerType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export function useVisualizerSystemAudio(options: {
|
|
||||||
onSystemAudioCaptureDenied?: () => void;
|
|
||||||
onSystemAudioCaptureSuccess?: () => void;
|
|
||||||
shouldAttemptConnection: boolean;
|
|
||||||
}) {
|
|
||||||
const { onSystemAudioCaptureDenied, onSystemAudioCaptureSuccess, shouldAttemptConnection } =
|
|
||||||
options;
|
|
||||||
const onDeniedRef = useRef(onSystemAudioCaptureDenied);
|
|
||||||
const onSuccessRef = useRef(onSystemAudioCaptureSuccess);
|
|
||||||
onDeniedRef.current = onSystemAudioCaptureDenied;
|
|
||||||
onSuccessRef.current = onSystemAudioCaptureSuccess;
|
|
||||||
const playbackType = usePlaybackType();
|
|
||||||
const { setWebAudio, webAudio } = useWebAudio();
|
|
||||||
const webAudioRef = useRef(webAudio);
|
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
|
||||||
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
|
||||||
const connectInFlightRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
webAudioRef.current = webAudio;
|
|
||||||
}, [webAudio]);
|
|
||||||
|
|
||||||
const disconnect = useCallback(() => {
|
|
||||||
if (streamRef.current) {
|
|
||||||
streamRef.current.getTracks().forEach((t) => t.stop());
|
|
||||||
streamRef.current = null;
|
|
||||||
}
|
|
||||||
if (sourceRef.current) {
|
|
||||||
try {
|
|
||||||
sourceRef.current.disconnect();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
sourceRef.current = null;
|
|
||||||
}
|
|
||||||
const w = webAudioRef.current;
|
|
||||||
if (w?.visualizerInputs?.length && setWebAudio) {
|
|
||||||
const next = { ...w, visualizerInputs: undefined };
|
|
||||||
setWebAudio(next);
|
|
||||||
webAudioRef.current = next;
|
|
||||||
}
|
|
||||||
}, [setWebAudio]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (playbackType === PlayerType.WEB || !shouldAttemptConnection) {
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
}, [playbackType, shouldAttemptConnection, disconnect]);
|
|
||||||
|
|
||||||
const connect = useCallback(async () => {
|
|
||||||
if (!isElectron()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const w = webAudioRef.current;
|
|
||||||
if (!w?.context || w.context.state === 'closed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!setWebAudio) return;
|
|
||||||
|
|
||||||
disconnect();
|
|
||||||
|
|
||||||
const wAfterDisconnect = webAudioRef.current;
|
|
||||||
if (!wAfterDisconnect?.context || wAfterDisconnect.context.state === 'closed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectInFlightRef.current = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
||||||
audio: true,
|
|
||||||
video: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const audioTracks = stream.getAudioTracks();
|
|
||||||
if (audioTracks.length === 0) {
|
|
||||||
stream.getTracks().forEach((t) => t.stop());
|
|
||||||
onDeniedRef.current?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latest = webAudioRef.current;
|
|
||||||
if (!latest?.context || latest.context.state === 'closed') {
|
|
||||||
stream.getTracks().forEach((t) => t.stop());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await latest.context.resume();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = latest.context.createMediaStreamSource(stream);
|
|
||||||
streamRef.current = stream;
|
|
||||||
sourceRef.current = source;
|
|
||||||
|
|
||||||
const next = { ...latest, visualizerInputs: [source] };
|
|
||||||
setWebAudio(next);
|
|
||||||
webAudioRef.current = next;
|
|
||||||
onSuccessRef.current?.();
|
|
||||||
} catch (e) {
|
|
||||||
const name = (e as DOMException)?.name;
|
|
||||||
if (name === 'NotAllowedError' || name === 'AbortError') {
|
|
||||||
onDeniedRef.current?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.error({
|
|
||||||
message: i18n.t('visualizer.systemAudioCaptureFailed', {
|
|
||||||
message: (e as Error).message,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
connectInFlightRef.current = false;
|
|
||||||
}
|
|
||||||
}, [disconnect, setWebAudio]);
|
|
||||||
|
|
||||||
const connectRef = useRef(connect);
|
|
||||||
connectRef.current = connect;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (playbackType !== PlayerType.LOCAL || !isElectron() || !shouldAttemptConnection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const w = webAudioRef.current;
|
|
||||||
if (!w?.context || w.context.state === 'closed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (w.visualizerInputs?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (connectInFlightRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void connectRef.current();
|
|
||||||
}, [
|
|
||||||
playbackType,
|
|
||||||
shouldAttemptConnection,
|
|
||||||
webAudio?.context,
|
|
||||||
webAudio?.visualizerInputs?.length,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { useFullScreenPlayerStore, useSettingsStore } from '/@/renderer/store';
|
|
||||||
|
|
||||||
export function closeLocalVisualizerSurfaces(): void {
|
|
||||||
const fullScreen = useFullScreenPlayerStore.getState();
|
|
||||||
fullScreen.actions.setStore({
|
|
||||||
...(fullScreen.expanded && fullScreen.activeTab === 'visualizer'
|
|
||||||
? { activeTab: 'queue' as const }
|
|
||||||
: {}),
|
|
||||||
visualizerExpanded: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useSettingsStore.getState().actions.setSettings({
|
|
||||||
general: { showVisualizerInSidebar: false },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { WebAudio } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
import { PlayerType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export function getVisualizerAudioNodes(
|
|
||||||
webAudio: undefined | WebAudio,
|
|
||||||
playbackType: PlayerType,
|
|
||||||
): AudioNode[] {
|
|
||||||
if (!webAudio) return [];
|
|
||||||
if (playbackType === PlayerType.LOCAL) {
|
|
||||||
return webAudio.visualizerInputs ?? [];
|
|
||||||
}
|
|
||||||
return webAudio.gains;
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import i18n from '/@/i18n/i18n';
|
|||||||
export const openVisualizerSettingsModal = () => {
|
export const openVisualizerSettingsModal = () => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: {},
|
innerProps: {},
|
||||||
modal: 'visualizerSettings',
|
modalKey: 'visualizerSettings',
|
||||||
overlayProps: {
|
overlayProps: {
|
||||||
blur: 0,
|
blur: 0,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListGridProps extends Omit<
|
interface PlaylistDetailSongListGridProps
|
||||||
ItemListGridComponentProps<PlaylistSongListQuery>,
|
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||||
'query'
|
|
||||||
> {
|
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
data: PlaylistSongListResponse;
|
data: PlaylistSongListResponse;
|
||||||
items?: Song[];
|
items?: Song[];
|
||||||
|
|||||||
+7
-7
@@ -60,12 +60,12 @@ const PlaylistSongListFiltersModal = () => {
|
|||||||
const hasActiveFilters = useMemo(() => {
|
const hasActiveFilters = useMemo(() => {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
|
||||||
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
|
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
|
||||||
query[FILTER_KEYS.SONG.HAS_RATING] !== undefined ||
|
query[FILTER_KEYS.SONG.HAS_RATING] !== undefined ||
|
||||||
query[FILTER_KEYS.SONG.MAX_YEAR] !== undefined ||
|
query[FILTER_KEYS.SONG.MAX_YEAR] !== undefined ||
|
||||||
query[FILTER_KEYS.SONG.MIN_YEAR] !== undefined,
|
query[FILTER_KEYS.SONG.MIN_YEAR] !== undefined,
|
||||||
);
|
);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ export const openSaveAndReplaceModal = (
|
|||||||
) => {
|
) => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: { onSuccess, playlistId, songIds },
|
innerProps: { onSuccess, playlistId, songIds },
|
||||||
modal: 'saveAndReplace',
|
modalKey: 'saveAndReplace',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
|
import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListTableProps extends Omit<
|
interface PlaylistDetailSongListTableProps
|
||||||
ItemListTableComponentProps<PlaylistSongListQuery>,
|
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||||
'query'
|
|
||||||
> {
|
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
data: PlaylistSongListResponse;
|
data: PlaylistSongListResponse;
|
||||||
items?: Song[];
|
items?: Song[];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ import { toast } from '/@/shared/components/toast/toast';
|
|||||||
import { SongListSort } from '/@/shared/types/domain-types';
|
import { SongListSort } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export interface PlaylistQueryEditorProps {
|
export interface PlaylistQueryEditorProps {
|
||||||
detailQuery: UseSuspenseQueryResult<any, Error>;
|
detailQuery: ReturnType<typeof useQuery<any>>;
|
||||||
handleSave: (
|
handleSave: (
|
||||||
filter: Record<string, any>,
|
filter: Record<string, any>,
|
||||||
extraFilters: {
|
extraFilters: {
|
||||||
@@ -422,12 +421,8 @@ export const PlaylistQueryEditor = ({
|
|||||||
minRows={8}
|
minRows={8}
|
||||||
onChange={(value) => setJsonText(value)}
|
onChange={(value) => setJsonText(value)}
|
||||||
placeholder='{ "all": [], "limit": 100, "sort": "+dateAdded" }'
|
placeholder='{ "all": [], "limit": 100, "sort": "+dateAdded" }'
|
||||||
size="lg"
|
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
style={{
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
flex: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
}}
|
|
||||||
value={jsonText}
|
value={jsonText}
|
||||||
/>
|
/>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const openUpdatePlaylistModal = async (args: { playlist: Playlist }) => {
|
|||||||
},
|
},
|
||||||
query: { id: playlist?.id },
|
query: { id: playlist?.id },
|
||||||
},
|
},
|
||||||
modal: 'updatePlaylist',
|
modalKey: 'updatePlaylist',
|
||||||
size: hasImageUpload ? 'lg' : 'md',
|
size: hasImageUpload ? 'lg' : 'md',
|
||||||
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
|
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-f
|
|||||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { parseCustomFiltersParam } from '/@/renderer/utils/query-params';
|
import { parseCustomFiltersParam } from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { PlaylistListSort } from '/@/shared/types/domain-types';
|
import { PlaylistListSort } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -25,30 +24,28 @@ export const usePlaylistListFilters = () => {
|
|||||||
|
|
||||||
const setCustom = useCallback(
|
const setCustom = useCallback(
|
||||||
(value: null | Record<string, any>) => {
|
(value: null | Record<string, any>) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM);
|
||||||
const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM);
|
|
||||||
|
|
||||||
const newCustom = {
|
const newCustom = {
|
||||||
...(previousValue ? JSON.parse(previousValue) : {}),
|
...(previousValue ? JSON.parse(previousValue) : {}),
|
||||||
...value,
|
...value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredNewCustom = Object.fromEntries(
|
const filteredNewCustom = Object.fromEntries(
|
||||||
Object.entries(newCustom).filter(
|
Object.entries(newCustom).filter(
|
||||||
([, value]) => value !== null && value !== undefined,
|
([, value]) => value !== null && value !== undefined,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom));
|
prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom));
|
||||||
return prev;
|
return prev;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replace: true,
|
replace: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
setMultipleSearchParams,
|
setMultipleSearchParams,
|
||||||
setSearchParam,
|
setSearchParam,
|
||||||
} from '/@/renderer/utils/query-params';
|
} from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -75,22 +74,18 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setAlbumArtistIds = useCallback(
|
const setAlbumArtistIds = useCallback(
|
||||||
(value: null | string[]) => {
|
(value: null | string[]) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_ARTIST_IDS, value),
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.SONG.ALBUM_ARTIST_IDS, value),
|
{ replace: true },
|
||||||
{ replace: true },
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setGenreId = useCallback(
|
const setGenreId = useCallback(
|
||||||
(value: null | string[]) => {
|
(value: null | string[]) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.GENRE_ID, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -98,13 +93,8 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setArtistIds = useCallback(
|
const setArtistIds = useCallback(
|
||||||
(value: null | string[]) => {
|
(value: null | string[]) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.ARTIST_IDS, value), {
|
||||||
setSearchParams(
|
replace: true,
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.SONG.ARTIST_IDS, value),
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -112,10 +102,8 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setMinYear = useCallback(
|
const setMinYear = useCallback(
|
||||||
(value: null | number) => {
|
(value: null | number) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MIN_YEAR, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MIN_YEAR, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -123,10 +111,8 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setMaxYear = useCallback(
|
const setMaxYear = useCallback(
|
||||||
(value: null | number) => {
|
(value: null | number) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MAX_YEAR, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.MAX_YEAR, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -134,10 +120,8 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setFavorite = useCallback(
|
const setFavorite = useCallback(
|
||||||
(value: boolean | null) => {
|
(value: boolean | null) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.FAVORITE, value), {
|
||||||
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.FAVORITE, value), {
|
replace: true,
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -145,13 +129,8 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setHasRating = useCallback(
|
const setHasRating = useCallback(
|
||||||
(value: boolean | null) => {
|
(value: boolean | null) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), {
|
||||||
setSearchParams(
|
replace: true,
|
||||||
(prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value),
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
@@ -174,55 +153,51 @@ export const usePlaylistSongListFilters = () => {
|
|||||||
|
|
||||||
const setCustom = useCallback(
|
const setCustom = useCallback(
|
||||||
(value: null | Record<string, any>) => {
|
(value: null | Record<string, any>) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM);
|
||||||
const previousValue = prev.get(FILTER_KEYS.ALBUM._CUSTOM);
|
|
||||||
|
|
||||||
const newCustom = {
|
const newCustom = {
|
||||||
...(previousValue ? JSON.parse(previousValue) : {}),
|
...(previousValue ? JSON.parse(previousValue) : {}),
|
||||||
...value,
|
...value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredNewCustom = Object.fromEntries(
|
const filteredNewCustom = Object.fromEntries(
|
||||||
Object.entries(newCustom).filter(
|
Object.entries(newCustom).filter(
|
||||||
([, value]) => value !== null && value !== undefined,
|
([, value]) => value !== null && value !== undefined,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom));
|
prev.set(FILTER_KEYS.ALBUM._CUSTOM, JSON.stringify(filteredNewCustom));
|
||||||
return prev;
|
return prev;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replace: true,
|
replace: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) =>
|
||||||
(prev) =>
|
setMultipleSearchParams(
|
||||||
setMultipleSearchParams(
|
prev,
|
||||||
prev,
|
{
|
||||||
{
|
[FILTER_KEYS.SONG._CUSTOM]: null,
|
||||||
[FILTER_KEYS.SONG._CUSTOM]: null,
|
[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: null,
|
||||||
[FILTER_KEYS.SONG.ALBUM_ARTIST_IDS]: null,
|
[FILTER_KEYS.SONG.ARTIST_IDS]: null,
|
||||||
[FILTER_KEYS.SONG.ARTIST_IDS]: null,
|
[FILTER_KEYS.SONG.FAVORITE]: null,
|
||||||
[FILTER_KEYS.SONG.FAVORITE]: null,
|
[FILTER_KEYS.SONG.GENRE_ID]: null,
|
||||||
[FILTER_KEYS.SONG.GENRE_ID]: null,
|
[FILTER_KEYS.SONG.HAS_RATING]: null,
|
||||||
[FILTER_KEYS.SONG.HAS_RATING]: null,
|
[FILTER_KEYS.SONG.MAX_YEAR]: null,
|
||||||
[FILTER_KEYS.SONG.MAX_YEAR]: null,
|
[FILTER_KEYS.SONG.MIN_YEAR]: null,
|
||||||
[FILTER_KEYS.SONG.MIN_YEAR]: null,
|
},
|
||||||
},
|
new Set([FILTER_KEYS.SONG._CUSTOM]),
|
||||||
new Set([FILTER_KEYS.SONG._CUSTOM]),
|
),
|
||||||
),
|
{ replace: true },
|
||||||
{ replace: true },
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [setSearchParams]);
|
}, [setSearchParams]);
|
||||||
|
|
||||||
const query = useMemo(
|
const query = useMemo(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Suspense, useMemo, useRef, useState } from 'react';
|
import { Suspense, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
import { generatePath, useLocation, useNavigate, useParams } from 'react-router';
|
||||||
|
|
||||||
import { ListContext, useListContext } from '/@/renderer/context/list-context';
|
import { ListContext, useListContext } from '/@/renderer/context/list-context';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
@@ -72,11 +72,13 @@ const PlaylistSongListFiltersSidebar = () => {
|
|||||||
const PlaylistDetailSongListRoute = () => {
|
const PlaylistDetailSongListRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const detailQuery = useSuspenseQuery({
|
const detailQuery = useQuery({
|
||||||
...playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
...playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
||||||
|
placeholderData: location.state?.item,
|
||||||
});
|
});
|
||||||
const deletePlaylistMutation = useDeletePlaylist({});
|
const deletePlaylistMutation = useDeletePlaylist({});
|
||||||
const updatePlaylistMutation = useUpdatePlaylist({});
|
const updatePlaylistMutation = useUpdatePlaylist({});
|
||||||
@@ -210,7 +212,9 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isSmartPlaylist = Boolean(
|
const isSmartPlaylist = Boolean(
|
||||||
detailQuery?.data?.rules && server?.type === ServerType.NAVIDROME,
|
!detailQuery?.isLoading &&
|
||||||
|
detailQuery?.data?.rules &&
|
||||||
|
server?.type === ServerType.NAVIDROME,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
font-size: var(--theme-font-size-sm);
|
font-size: var(--theme-font-size-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading:hover {
|
.heading:hover {
|
||||||
@@ -38,7 +39,3 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items button:focus-visible {
|
|
||||||
outline: 2px solid var(--theme-colors-primary);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Command } from 'cmdk';
|
import { Command } from 'cmdk';
|
||||||
import { ComponentPropsWithoutRef, ReactNode, useEffect, useRef, useState } from 'react';
|
import { ComponentPropsWithoutRef, ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
interface CommandItemSelectableProps extends Omit<
|
interface CommandItemSelectableProps
|
||||||
ComponentPropsWithoutRef<typeof Command.Item>,
|
extends Omit<ComponentPropsWithoutRef<typeof Command.Item>, 'children'> {
|
||||||
'children'
|
|
||||||
> {
|
|
||||||
children: (args: { isHighlighted: boolean }) => ReactNode;
|
children: (args: { isHighlighted: boolean }) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useDeferredValue, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
|
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
|
||||||
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
|
import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands';
|
||||||
@@ -49,7 +49,6 @@ function CommandPaletteSearch({
|
|||||||
setQuery,
|
setQuery,
|
||||||
}: CommandPaletteSearchProps) {
|
}: CommandPaletteSearchProps) {
|
||||||
const [debouncedQuery] = useDebouncedValue(query, 400);
|
const [debouncedQuery] = useDebouncedValue(query, 400);
|
||||||
const deferredSearchQuery = useDeferredValue(debouncedQuery ?? '');
|
|
||||||
const searchSectionsExpanded = useAppStore(
|
const searchSectionsExpanded = useAppStore(
|
||||||
(state) => state.commandPaletteSearchSectionsExpanded,
|
(state) => state.commandPaletteSearchSectionsExpanded,
|
||||||
);
|
);
|
||||||
@@ -84,7 +83,7 @@ function CommandPaletteSearch({
|
|||||||
<Command.List>
|
<Command.List>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<SearchAlbumsSection
|
<SearchAlbumsSection
|
||||||
debouncedQuery={deferredSearchQuery}
|
debouncedQuery={debouncedQuery ?? ''}
|
||||||
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true}
|
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true}
|
||||||
isHome={isHome}
|
isHome={isHome}
|
||||||
onSelectResult={onSelectResult}
|
onSelectResult={onSelectResult}
|
||||||
@@ -97,7 +96,7 @@ function CommandPaletteSearch({
|
|||||||
query={query}
|
query={query}
|
||||||
/>
|
/>
|
||||||
<SearchAlbumArtistsSection
|
<SearchAlbumArtistsSection
|
||||||
debouncedQuery={deferredSearchQuery}
|
debouncedQuery={debouncedQuery ?? ''}
|
||||||
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true}
|
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true}
|
||||||
isHome={isHome}
|
isHome={isHome}
|
||||||
onSelectResult={onSelectResult}
|
onSelectResult={onSelectResult}
|
||||||
@@ -110,7 +109,7 @@ function CommandPaletteSearch({
|
|||||||
query={query}
|
query={query}
|
||||||
/>
|
/>
|
||||||
<SearchSongsSection
|
<SearchSongsSection
|
||||||
debouncedQuery={deferredSearchQuery}
|
debouncedQuery={debouncedQuery ?? ''}
|
||||||
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true}
|
expanded={searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true}
|
||||||
isHome={isHome}
|
isHome={isHome}
|
||||||
onSelectResult={onSelectResult}
|
onSelectResult={onSelectResult}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const ServerList = () => {
|
|||||||
<AddServerForm onCancel={() => vars.context.closeModal(vars.id)} />
|
<AddServerForm onCancel={() => vars.context.closeModal(vars.id)} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
modal: 'base',
|
modalKey: 'base',
|
||||||
title: t('form.addServer.title', { postProcess: 'titleCase' }),
|
title: t('form.addServer.title', { postProcess: 'titleCase' }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { openContextModal } from '@mantine/modals';
|
|||||||
export const openSettingsModal = () => {
|
export const openSettingsModal = () => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: {},
|
innerProps: {},
|
||||||
modal: 'settings',
|
modalKey: 'settings',
|
||||||
overlayProps: {
|
overlayProps: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const AnimatedPage = forwardRef(
|
|||||||
<motion.main
|
<motion.main
|
||||||
className={styles.animatedPage}
|
className={styles.animatedPage}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...{ ...animationProps.fadeIn, transition: { duration: 0.5, ease: 'anticipate' } }}
|
{...{ ...animationProps.fadeIn, transition: { duration: 0.3, ease: 'anticipate' } }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.main>
|
</motion.main>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.filter-bar {
|
.filter-bar {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: var(--theme-spacing-xs);
|
padding: var(--theme-spacing-sm);
|
||||||
border-bottom: 1px solid var(--theme-colors-border);
|
border-bottom: 1px solid var(--theme-colors-border);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--theme-content-max-width);
|
max-width: var(--theme-content-max-width);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 var(--theme-spacing-sm);
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-button-container {
|
.play-button-container {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
import { createContext, ReactNode, useContext, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import styles from './list-with-sidebar-container.module.css';
|
import styles from './list-with-sidebar-container.module.css';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import { animationProps } from '/@/shared/components/animations/animation-props'
|
|||||||
import { Portal } from '/@/shared/components/portal/portal';
|
import { Portal } from '/@/shared/components/portal/portal';
|
||||||
|
|
||||||
interface ListWithSidebarContainerContextValue {
|
interface ListWithSidebarContainerContextValue {
|
||||||
sidebarElement: HTMLDivElement | null;
|
sidebarRef: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListWithSidebarContainerContext = createContext<ListWithSidebarContainerContextValue | null>(
|
const ListWithSidebarContainerContext = createContext<ListWithSidebarContainerContextValue | null>(
|
||||||
@@ -36,12 +36,12 @@ function Sidebar({ children }: SidebarProps) {
|
|||||||
throw new Error('Sidebar must be used within ListWithSidebarContainer');
|
throw new Error('Sidebar must be used within ListWithSidebarContainer');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.sidebarElement) {
|
if (!context.sidebarRef?.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal target={context.sidebarElement}>
|
<Portal target={context.sidebarRef.current}>
|
||||||
<motion.div {...animationProps.slideInLeft} style={{ height: '100%', width: '100%' }}>
|
<motion.div {...animationProps.slideInLeft} style={{ height: '100%', width: '100%' }}>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -56,25 +56,25 @@ function SidebarPortal({ children }: SidebarPortalProps) {
|
|||||||
throw new Error('SidebarPortal must be used within ListWithSidebarContainer');
|
throw new Error('SidebarPortal must be used within ListWithSidebarContainer');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.sidebarElement) {
|
if (!context.sidebarRef?.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Portal target={context.sidebarElement}>{children}</Portal>;
|
return <Portal target={context.sidebarRef.current}>{children}</Portal>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListWithSidebarContainer = ({
|
export const ListWithSidebarContainer = ({
|
||||||
children,
|
children,
|
||||||
useBreakpoint = false,
|
useBreakpoint = false,
|
||||||
}: ListWithSidebarContainerProps) => {
|
}: ListWithSidebarContainerProps) => {
|
||||||
const [sidebarElement, setSidebarElement] = useState<HTMLDivElement | null>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
const { isSidebarOpen = false } = useListContext();
|
const { isSidebarOpen = false } = useListContext();
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
sidebarElement,
|
sidebarRef,
|
||||||
}),
|
}),
|
||||||
[sidebarElement],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +84,7 @@ export const ListWithSidebarContainer = ({
|
|||||||
data-sidebar-open={useBreakpoint ? undefined : isSidebarOpen}
|
data-sidebar-open={useBreakpoint ? undefined : isSidebarOpen}
|
||||||
data-use-breakpoint={useBreakpoint}
|
data-use-breakpoint={useBreakpoint}
|
||||||
>
|
>
|
||||||
<div className={styles.sidebarContainer} ref={setSidebarElement} />
|
<div className={styles.sidebarContainer} ref={sidebarRef} />
|
||||||
<div className={styles.contentContainer}>{children}</div>
|
<div className={styles.contentContainer}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</ListWithSidebarContainerContext.Provider>
|
</ListWithSidebarContainerContext.Provider>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|||||||
interface SearchInputProps extends TextInputProps {
|
interface SearchInputProps extends TextInputProps {
|
||||||
buttonProps?: Partial<ActionIconProps>;
|
buttonProps?: Partial<ActionIconProps>;
|
||||||
enableHotkey?: boolean;
|
enableHotkey?: boolean;
|
||||||
fillContainer?: boolean;
|
|
||||||
inputProps?: Partial<TextInputProps>;
|
inputProps?: Partial<TextInputProps>;
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
@@ -27,7 +26,6 @@ interface SearchInputProps extends TextInputProps {
|
|||||||
export const SearchInput = ({
|
export const SearchInput = ({
|
||||||
buttonProps,
|
buttonProps,
|
||||||
enableHotkey = true,
|
enableHotkey = true,
|
||||||
fillContainer = false,
|
|
||||||
inputProps,
|
inputProps,
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
...props
|
||||||
@@ -106,17 +104,9 @@ export const SearchInput = ({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
transition: 'width 0.3s ease-in-out',
|
transition: 'width 0.3s ease-in-out',
|
||||||
...(fillContainer
|
width: shouldExpand ? '200px' : '36px',
|
||||||
? {
|
|
||||||
flex: '1 1 0',
|
|
||||||
minWidth: 0,
|
|
||||||
width: shouldExpand ? '100%' : '36px',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
width: shouldExpand ? '200px' : '36px',
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
[fillContainer, shouldExpand],
|
[shouldExpand],
|
||||||
);
|
);
|
||||||
|
|
||||||
const buttonStyle: CSSProperties = useMemo(
|
const buttonStyle: CSSProperties = useMemo(
|
||||||
@@ -145,7 +135,7 @@ export const SearchInput = ({
|
|||||||
<Box style={containerStyle}>
|
<Box style={containerStyle}>
|
||||||
<TextInput
|
<TextInput
|
||||||
leftSection={<Icon icon="search" />}
|
leftSection={<Icon icon="search" />}
|
||||||
maw={fillContainer ? '100%' : '20dvw'}
|
maw="20dvw"
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router';
|
|||||||
|
|
||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params';
|
import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
|
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
|
||||||
|
|
||||||
export const useSearchTermFilter = (defaultValue?: string) => {
|
export const useSearchTermFilter = (defaultValue?: string) => {
|
||||||
@@ -15,19 +14,17 @@ export const useSearchTermFilter = (defaultValue?: string) => {
|
|||||||
}, [searchParams, defaultValue]);
|
}, [searchParams, defaultValue]);
|
||||||
|
|
||||||
const handleSetSearchTerm = (value: null | string) => {
|
const handleSetSearchTerm = (value: null | string) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const newParams = setSearchParam(
|
||||||
const newParams = setSearchParam(
|
prev,
|
||||||
prev,
|
FILTER_KEYS.SHARED.SEARCH_TERM,
|
||||||
FILTER_KEYS.SHARED.SEARCH_TERM,
|
value === '' ? null : value,
|
||||||
value === '' ? null : value,
|
);
|
||||||
);
|
return newParams;
|
||||||
return newParams;
|
},
|
||||||
},
|
{ replace: true },
|
||||||
{ replace: true },
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedSetSearchTerm = useDebouncedCallback(handleSetSearchTerm, 300);
|
const debouncedSetSearchTerm = useDebouncedCallback(handleSetSearchTerm, 300);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
@@ -8,26 +8,21 @@ export const useSetFavorite = () => {
|
|||||||
const createFavoriteMutation = useCreateFavorite({});
|
const createFavoriteMutation = useCreateFavorite({});
|
||||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||||
|
|
||||||
const createFavoriteMutationRef = useRef(createFavoriteMutation);
|
|
||||||
const deleteFavoriteMutationRef = useRef(deleteFavoriteMutation);
|
|
||||||
createFavoriteMutationRef.current = createFavoriteMutation;
|
|
||||||
deleteFavoriteMutationRef.current = deleteFavoriteMutation;
|
|
||||||
|
|
||||||
const setFavorite = useCallback(
|
const setFavorite = useCallback(
|
||||||
(serverId: string, id: string[], itemType: LibraryItem, isFavorite: boolean) => {
|
(serverId: string, id: string[], itemType: LibraryItem, isFavorite: boolean) => {
|
||||||
if (isFavorite) {
|
if (isFavorite) {
|
||||||
createFavoriteMutationRef.current.mutate({
|
createFavoriteMutation.mutate({
|
||||||
apiClientProps: { serverId },
|
apiClientProps: { serverId },
|
||||||
query: { id, type: itemType },
|
query: { id, type: itemType },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
deleteFavoriteMutationRef.current.mutate({
|
deleteFavoriteMutation.mutate({
|
||||||
apiClientProps: { serverId },
|
apiClientProps: { serverId },
|
||||||
query: { id, type: itemType },
|
query: { id, type: itemType },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[createFavoriteMutation, deleteFavoriteMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return setFavorite;
|
return setFavorite;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useSetRatingMutation } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
import { useSetRatingMutation } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
@@ -6,17 +6,14 @@ import { LibraryItem } from '/@/shared/types/domain-types';
|
|||||||
export const useSetRating = () => {
|
export const useSetRating = () => {
|
||||||
const setRatingMutation = useSetRatingMutation({});
|
const setRatingMutation = useSetRatingMutation({});
|
||||||
|
|
||||||
const setRatingMutationRef = useRef(setRatingMutation);
|
|
||||||
setRatingMutationRef.current = setRatingMutation;
|
|
||||||
|
|
||||||
const setRating = useCallback(
|
const setRating = useCallback(
|
||||||
(serverId: string, id: string[], itemType: LibraryItem, rating: number) => {
|
(serverId: string, id: string[], itemType: LibraryItem, rating: number) => {
|
||||||
setRatingMutationRef.current.mutate({
|
setRatingMutation.mutate({
|
||||||
apiClientProps: { serverId },
|
apiClientProps: { serverId },
|
||||||
query: { id, rating, type: itemType },
|
query: { id, rating, type: itemType },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[],
|
[setRatingMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return setRating;
|
return setRating;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use-
|
|||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params';
|
import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const useSortByFilter = <TSortBy>(defaultValue: null | string, listKey: ItemListKey) => {
|
export const useSortByFilter = <TSortBy>(defaultValue: null | string, listKey: ItemListKey) => {
|
||||||
@@ -21,16 +20,14 @@ export const useSortByFilter = <TSortBy>(defaultValue: null | string, listKey: I
|
|||||||
}, [searchParams, persisted, defaultValue]);
|
}, [searchParams, persisted, defaultValue]);
|
||||||
|
|
||||||
const handleSetSortBy = (sortBy: string) => {
|
const handleSetSortBy = (sortBy: string) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const newParams = setSearchParam(prev, FILTER_KEYS.SHARED.SORT_BY, sortBy);
|
||||||
const newParams = setSearchParam(prev, FILTER_KEYS.SHARED.SORT_BY, sortBy);
|
return newParams;
|
||||||
return newParams;
|
},
|
||||||
},
|
{ replace: true },
|
||||||
{ replace: true },
|
);
|
||||||
);
|
setFilter(FILTER_KEYS.SHARED.SORT_BY, sortBy);
|
||||||
setFilter(FILTER_KEYS.SHARED.SORT_BY, sortBy);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use-
|
|||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params';
|
import { parseStringParam, setSearchParam } from '/@/renderer/utils/query-params';
|
||||||
import { runInUrlTransition } from '/@/renderer/utils/url-transition';
|
|
||||||
import { SortOrder } from '/@/shared/types/domain-types';
|
import { SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -22,20 +21,14 @@ export const useSortOrderFilter = (defaultValue: null | string, listKey: ItemLis
|
|||||||
}, [searchParams, persisted, defaultValue]);
|
}, [searchParams, persisted, defaultValue]);
|
||||||
|
|
||||||
const handleSetSortOrder = (sortOrder: SortOrder) => {
|
const handleSetSortOrder = (sortOrder: SortOrder) => {
|
||||||
runInUrlTransition(() => {
|
setSearchParams(
|
||||||
setSearchParams(
|
(prev) => {
|
||||||
(prev) => {
|
const newParams = setSearchParam(prev, FILTER_KEYS.SHARED.SORT_ORDER, sortOrder);
|
||||||
const newParams = setSearchParam(
|
return newParams;
|
||||||
prev,
|
},
|
||||||
FILTER_KEYS.SHARED.SORT_ORDER,
|
{ replace: true },
|
||||||
sortOrder,
|
);
|
||||||
);
|
setFilter(FILTER_KEYS.SHARED.SORT_ORDER, sortOrder);
|
||||||
return newParams;
|
|
||||||
},
|
|
||||||
{ replace: true },
|
|
||||||
);
|
|
||||||
setFilter(FILTER_KEYS.SHARED.SORT_ORDER, sortOrder);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,8 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accordion-control {
|
.accordion-control {
|
||||||
height: auto;
|
height: 2.5rem;
|
||||||
min-height: 2.5rem;
|
|
||||||
border-radius: var(--theme-radius-md);
|
border-radius: var(--theme-radius-md);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -39,3 +38,9 @@
|
|||||||
.accordion-content:last-child {
|
.accordion-content:last-child {
|
||||||
padding-bottom: var(--theme-spacing-md);
|
padding-bottom: var(--theme-spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-selector-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import styles from './mobile-sidebar.module.css';
|
import styles from './mobile-sidebar.module.css';
|
||||||
|
|
||||||
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
||||||
|
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
||||||
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
||||||
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
||||||
import {
|
import {
|
||||||
@@ -103,6 +104,9 @@ export const MobileSidebar = () => {
|
|||||||
)}
|
)}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
<div className={styles.serverSelectorWrapper}>
|
||||||
|
<ServerSelector />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
.button-container {
|
.button-container {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: var(--theme-spacing-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-container-no-bottom-padding {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
padding: var(--theme-spacing-sm);
|
padding: var(--theme-spacing-sm);
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group:hover {
|
|
||||||
background: var(--theme-colors-surface);
|
background: var(--theme-colors-surface);
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import styles from './server-selector.module.css';
|
import styles from './server-selector.module.css';
|
||||||
@@ -8,7 +9,7 @@ import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png';
|
|||||||
import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png';
|
import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import { ServerSelectorItems } from '/@/renderer/features/sidebar/components/server-selector-items';
|
import { ServerSelectorItems } from '/@/renderer/features/sidebar/components/server-selector-items';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useAppStore, useCurrentServer } from '/@/renderer/store';
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||||
@@ -23,6 +24,8 @@ import { ServerFeature } from '/@/shared/types/features-types';
|
|||||||
export const ServerSelector = () => {
|
export const ServerSelector = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
|
const sidebarImageEnabled = useAppStore((state) => state.sidebar.image);
|
||||||
|
const showImage = sidebarImageEnabled;
|
||||||
|
|
||||||
const { data: musicFolders } = useQuery(
|
const { data: musicFolders } = useQuery(
|
||||||
currentServer
|
currentServer
|
||||||
@@ -30,6 +33,9 @@ export const ServerSelector = () => {
|
|||||||
: { enabled: false, queryKey: ['disabled'] },
|
: { enabled: false, queryKey: ['disabled'] },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const targetRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const widthOfTarget = targetRef.current?.getBoundingClientRect().width;
|
||||||
|
|
||||||
if (!currentServer) {
|
if (!currentServer) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -63,11 +69,15 @@ export const ServerSelector = () => {
|
|||||||
: OpenSubsonicLogo;
|
: OpenSubsonicLogo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu offset={0} position="right-start" withinPortal={false}>
|
<DropdownMenu offset={0} position="right">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<div className={styles.popoverTarget}>
|
<div className={styles.popoverTarget}>
|
||||||
<Box className={styles.buttonContainer}>
|
<Box
|
||||||
<Group className={styles.buttonGroup} gap="sm">
|
className={`${styles.buttonContainer} ${
|
||||||
|
showImage ? styles.buttonContainerNoBottomPadding : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Group className={styles.buttonGroup} gap="sm" ref={targetRef}>
|
||||||
<img className={styles.logo} src={logo} />
|
<img className={styles.logo} src={logo} />
|
||||||
<Stack className={styles.buttonStack} gap={2}>
|
<Stack className={styles.buttonStack} gap={2}>
|
||||||
<Text fw={600} size="sm" truncate>
|
<Text fw={600} size="sm" truncate>
|
||||||
@@ -82,7 +92,7 @@ export const ServerSelector = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown miw="16rem">
|
<DropdownMenu.Dropdown style={{ width: `${widthOfTarget}px` }}>
|
||||||
<ScrollArea className={styles.scrollArea}>
|
<ScrollArea className={styles.scrollArea}>
|
||||||
<ServerSelectorItems />
|
<ServerSelectorItems />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ const PlaylistRowButton = memo(
|
|||||||
|
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: modalProps,
|
innerProps: modalProps,
|
||||||
modal: 'addToPlaylist',
|
modalKey: 'addToPlaylist',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
title: t('form.addToPlaylist.title', { postProcess: 'titleCase' }),
|
title: t('form.addToPlaylist.title', { postProcess: 'titleCase' }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,8 +66,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accordion-control {
|
.accordion-control {
|
||||||
height: auto;
|
height: 2.5rem;
|
||||||
min-height: 2.5rem;
|
|
||||||
border-radius: var(--theme-radius-md);
|
border-radius: var(--theme-radius-md);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -83,3 +82,9 @@
|
|||||||
.accordion-content:last-child {
|
.accordion-content:last-child {
|
||||||
padding-bottom: var(--theme-spacing-md);
|
padding-bottom: var(--theme-spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-selector-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useRadioPlayer,
|
useRadioPlayer,
|
||||||
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||||
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
||||||
|
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
||||||
import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/sidebar-collection-list';
|
import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/sidebar-collection-list';
|
||||||
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
||||||
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
||||||
@@ -151,6 +152,9 @@ export const Sidebar = () => {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<AnimatePresence initial={false} mode="popLayout">
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
|
<motion.div className={styles.serverSelectorWrapper} key="server-selector" layout>
|
||||||
|
<ServerSelector />
|
||||||
|
</motion.div>
|
||||||
{showImage && <SidebarImage />}
|
{showImage && <SidebarImage />}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export const SongListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarget
|
|||||||
const query = songFilters.query;
|
const query = songFilters.query;
|
||||||
return Boolean(
|
return Boolean(
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG._CUSTOM]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG._CUSTOM]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
|
||||||
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
|
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.MAX_YEAR]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.MAX_YEAR]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SONG.MIN_YEAR]) ||
|
isFilterValueSet(query[FILTER_KEYS.SONG.MIN_YEAR]) ||
|
||||||
isFilterValueSet(query[FILTER_KEYS.SHARED.SEARCH_TERM]),
|
isFilterValueSet(query[FILTER_KEYS.SHARED.SEARCH_TERM]),
|
||||||
);
|
);
|
||||||
}, [songFilters.query]);
|
}, [songFilters.query]);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user