mirror of
https://github.com/antebudimir/feishin.git
synced 2026-03-01 03:37:26 +00:00
Migrate to Mantine v8 and Design Changes (#961)
* mantine v8 migration * various design changes and improvements
This commit is contained in:
parent
bea55d48a8
commit
c1330d92b2
473 changed files with 12469 additions and 11607 deletions
|
|
@ -1,17 +1,19 @@
|
||||||
{
|
{
|
||||||
"customSyntax": "postcss-styled-syntax",
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-standard",
|
"stylelint-config-standard",
|
||||||
"stylelint-config-styled-components",
|
"stylelint-config-css-modules",
|
||||||
"stylelint-config-recess-order"
|
"stylelint-config-recess-order"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"declaration-empty-line-before": null,
|
"block-no-empty": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
|
||||||
"selector-class-pattern": null,
|
|
||||||
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
||||||
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
||||||
"declaration-colon-newline-after": null,
|
"declaration-block-no-shorthand-property-overrides": null,
|
||||||
"property-no-vendor-prefix": null
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
|
"at-rule-no-unknown": [true, { "ignoreAtRules": ["mixin"] }],
|
||||||
|
"function-no-unknown": [true, { "ignoreFunctions": ["darken", "alpha", "lighten"] }],
|
||||||
|
"declaration-property-value-no-unknown": null,
|
||||||
|
"no-descending-specificity": null,
|
||||||
|
"no-empty-source": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
|
|
@ -26,10 +26,6 @@
|
||||||
"source.formatDocument": "explicit"
|
"source.formatDocument": "explicit"
|
||||||
},
|
},
|
||||||
"css.validate": true,
|
"css.validate": true,
|
||||||
"less.validate": false,
|
|
||||||
"scss.validate": true,
|
|
||||||
"scss.lint.unknownAtRules": "warning",
|
|
||||||
"scss.lint.unknownProperties": "warning",
|
|
||||||
"javascript.validate.enable": false,
|
"javascript.validate.enable": false,
|
||||||
"javascript.format.enable": false,
|
"javascript.format.enable": false,
|
||||||
"typescript.format.enable": false,
|
"typescript.format.enable": false,
|
||||||
|
|
@ -49,8 +45,14 @@
|
||||||
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locales"],
|
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locales"],
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"stylelint.validate": ["css", "scss", "typescript", "typescriptreact"],
|
"stylelint.config": null,
|
||||||
|
"stylelint.validate": ["css", "postcss"],
|
||||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||||
|
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||||
|
"@mantine/core",
|
||||||
|
"@mantine/modals",
|
||||||
|
"@mantine/dates"
|
||||||
|
],
|
||||||
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||||
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
|
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
|
||||||
"folderTemplates.structures": [
|
"folderTemplates.structures": [
|
||||||
|
|
@ -63,14 +65,14 @@
|
||||||
"template": "Functional Component with CSS Modules"
|
"template": "Functional Component with CSS Modules"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileName": "<FTName | kebabcase>.module.scss"
|
"fileName": "<FTName | kebabcase>.module.css"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"folderTemplates.fileTemplates": {
|
"folderTemplates.fileTemplates": {
|
||||||
"Functional Component with CSS Modules": [
|
"Functional Component with CSS Modules": [
|
||||||
"import styles from './<FTName | kebabcase>.module.scss';",
|
"import styles from './<FTName | kebabcase>.module.css';",
|
||||||
"",
|
"",
|
||||||
"interface <FTName | pascalcase>Props {}",
|
"interface <FTName | pascalcase>Props {}",
|
||||||
"",
|
"",
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const config: UserConfig = {
|
||||||
renderer: {
|
renderer: {
|
||||||
css: {
|
css: {
|
||||||
modules: {
|
modules: {
|
||||||
generateScopedName: '[name]__[local]__[hash:base64:5]',
|
generateScopedName: 'fs-[name]-[local]',
|
||||||
localsConvention: 'camelCase',
|
localsConvention: 'camelCase',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export default tseslint.config(
|
||||||
'react-refresh/only-export-components': 'off',
|
'react-refresh/only-export-components': 'off',
|
||||||
'react/display-name': 'off',
|
'react/display-name': 'off',
|
||||||
semi: ['error', 'always'],
|
semi: ['error', 'always'],
|
||||||
|
'single-attribute-per-line': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
eslintConfigPrettier,
|
eslintConfigPrettier,
|
||||||
|
|
|
||||||
67
package.json
67
package.json
|
|
@ -28,8 +28,12 @@
|
||||||
"dev:watch": "electron-vite dev --watch",
|
"dev:watch": "electron-vite dev --watch",
|
||||||
"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": "eslint --cache .",
|
"lint": "pnpm run lint-code && pnpm run lint-styles",
|
||||||
"lint:fix": "eslint --cache --fix .",
|
"lint-code": "eslint --cache .",
|
||||||
|
"lint-code:fix": "eslint --cache --fix .",
|
||||||
|
"lint-styles": "stylelint 'src/**/*.{css,scss}'",
|
||||||
|
"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",
|
||||||
|
|
@ -54,16 +58,18 @@
|
||||||
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
||||||
"@ag-grid-community/react": "^28.2.1",
|
"@ag-grid-community/react": "^28.2.1",
|
||||||
"@ag-grid-community/styles": "^28.2.1",
|
"@ag-grid-community/styles": "^28.2.1",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "1.4.0",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@emotion/react": "^11.10.4",
|
"@mantine/colors-generator": "^8.1.1",
|
||||||
"@mantine/core": "^6.0.22",
|
"@mantine/core": "^8.1.1",
|
||||||
"@mantine/dates": "^6.0.22",
|
"@mantine/dates": "^8.1.1",
|
||||||
"@mantine/form": "^6.0.22",
|
"@mantine/form": "^8.1.1",
|
||||||
"@mantine/hooks": "^6.0.22",
|
"@mantine/hooks": "^8.1.1",
|
||||||
"@mantine/modals": "^6.0.22",
|
"@mantine/modals": "^8.1.1",
|
||||||
"@mantine/notifications": "^6.0.22",
|
"@mantine/notifications": "^8.1.1",
|
||||||
"@mantine/utils": "^6.0.22",
|
|
||||||
"@tanstack/react-query": "^4.32.1",
|
"@tanstack/react-query": "^4.32.1",
|
||||||
"@tanstack/react-query-devtools": "^4.32.1",
|
"@tanstack/react-query-devtools": "^4.32.1",
|
||||||
"@tanstack/react-query-persist-client": "^4.32.1",
|
"@tanstack/react-query-persist-client": "^4.32.1",
|
||||||
|
|
@ -84,7 +90,6 @@
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"fast-average-color": "^9.3.0",
|
"fast-average-color": "^9.3.0",
|
||||||
"format-duration": "^2.0.0",
|
"format-duration": "^2.0.0",
|
||||||
"framer-motion": "^11.0.0",
|
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"i18next": "^21.10.0",
|
"i18next": "^21.10.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
|
|
@ -93,43 +98,46 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
|
"motion": "^12.18.1",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"nanoid": "^3.3.3",
|
"nanoid": "^3.3.3",
|
||||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||||
"overlayscrollbars": "^2.11.1",
|
"overlayscrollbars": "^2.11.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-i18next": "^11.18.6",
|
"react-i18next": "^11.18.6",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^5.5.0",
|
||||||
|
"react-image": "^4.1.0",
|
||||||
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-player": "^2.11.0",
|
"react-player": "^2.11.0",
|
||||||
"react-router": "^6.16.0",
|
"react-router": "^6.16.0",
|
||||||
"react-router-dom": "^6.16.0",
|
"react-router-dom": "^6.16.0",
|
||||||
"react-simple-img": "^3.0.0",
|
|
||||||
"react-virtualized-auto-sizer": "^1.0.17",
|
"react-virtualized-auto-sizer": "^1.0.17",
|
||||||
"react-window": "^1.8.9",
|
"react-window": "^1.8.9",
|
||||||
"react-window-infinite-loader": "^1.0.9",
|
"react-window-infinite-loader": "^1.0.9",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"styled-components": "^6.0.8",
|
|
||||||
"swiper": "^9.3.1",
|
"swiper": "^9.3.1",
|
||||||
|
"use-sync-external-store": "^1.5.0",
|
||||||
"ws": "^8.18.2",
|
"ws": "^8.18.2",
|
||||||
"zod": "^3.22.3",
|
"zod": "^3.22.3",
|
||||||
"zustand": "^4.3.9"
|
"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.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@types/electron-localshortcut": "^3.1.0",
|
"@types/electron-localshortcut": "^3.1.0",
|
||||||
"@types/lodash": "^4.14.188",
|
"@types/lodash": "^4.17.18",
|
||||||
"@types/md5": "^2.3.2",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^22.15.32",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"@types/react-window-infinite-loader": "^1.0.6",
|
"@types/react-window-infinite-loader": "^1.0.6",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/styled-components": "^5.1.26",
|
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"concurrently": "^7.1.0",
|
"concurrently": "^7.1.0",
|
||||||
|
|
@ -145,20 +153,15 @@
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"i18next-parser": "^9.0.2",
|
"i18next-parser": "^9.0.2",
|
||||||
"postcss-styled-syntax": "^0.5.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-packagejson": "^2.5.14",
|
"prettier-plugin-packagejson": "^2.5.14",
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"sass-embedded": "^1.89.0",
|
"sass-embedded": "^1.89.0",
|
||||||
"stylelint": "^15.10.3",
|
"stylelint": "^16.14.1",
|
||||||
"stylelint-config-css-modules": "^4.3.0",
|
"stylelint-config-css-modules": "^4.4.0",
|
||||||
"stylelint-config-recess-order": "^4.3.0",
|
"stylelint-config-recess-order": "^7.1.0",
|
||||||
"stylelint-config-standard": "^34.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-standard-scss": "^4.0.0",
|
|
||||||
"stylelint-config-styled-components": "^0.1.1",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-plugin-styled-components": "^3.0.0",
|
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"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",
|
||||||
|
|
@ -166,7 +169,9 @@
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
"abstract-socket",
|
||||||
"electron",
|
"electron",
|
||||||
|
"electron-winstaller",
|
||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
2009
pnpm-lock.yaml
generated
2009
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
5
postcss.config.cjs
Normal file
5
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
"action_other": "actions",
|
"action_other": "actions",
|
||||||
"add": "add",
|
"add": "add",
|
||||||
"additionalParticipants": "additional participants",
|
"additionalParticipants": "additional participants",
|
||||||
|
"newVersion": "a new version has been installed ({{version}})",
|
||||||
|
"viewReleaseNotes": "view release notes",
|
||||||
"albumGain": "album gain",
|
"albumGain": "album gain",
|
||||||
"albumPeak": "album peak",
|
"albumPeak": "album peak",
|
||||||
"areYouSure": "are you sure?",
|
"areYouSure": "are you sure?",
|
||||||
|
|
@ -268,6 +270,7 @@
|
||||||
"title": "lyric search"
|
"title": "lyric search"
|
||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
|
"title": "query editor",
|
||||||
"input_optionMatchAll": "match all",
|
"input_optionMatchAll": "match all",
|
||||||
"input_optionMatchAny": "match any"
|
"input_optionMatchAny": "match any"
|
||||||
},
|
},
|
||||||
|
|
@ -421,6 +424,7 @@
|
||||||
"folders": "$t(entity.folder_other)",
|
"folders": "$t(entity.folder_other)",
|
||||||
"genres": "$t(entity.genre_other)",
|
"genres": "$t(entity.genre_other)",
|
||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
|
"myLibrary": "my library",
|
||||||
"nowPlaying": "now playing",
|
"nowPlaying": "now playing",
|
||||||
"playlists": "$t(entity.playlist_other)",
|
"playlists": "$t(entity.playlist_other)",
|
||||||
"search": "$t(common.search)",
|
"search": "$t(common.search)",
|
||||||
|
|
@ -774,6 +778,8 @@
|
||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"card": "card",
|
"card": "card",
|
||||||
|
"grid": "grid",
|
||||||
|
"list": "list",
|
||||||
"poster": "poster",
|
"poster": "poster",
|
||||||
"table": "table"
|
"table": "table"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import './styles/global.scss';
|
import './styles/global.css';
|
||||||
|
|
||||||
import { Shell } from '/@/remote/components/shell';
|
import { Shell } from '/@/remote/components/shell';
|
||||||
import { useIsDark, useReconnect } from '/@/remote/store';
|
import { useIsDark, useReconnect } from '/@/remote/store';
|
||||||
|
|
@ -16,8 +16,8 @@ export const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
|
defaultColorScheme={isDark ? 'dark' : 'light'}
|
||||||
theme={{
|
theme={{
|
||||||
colorScheme: isDark ? 'dark' : 'light',
|
|
||||||
components: {
|
components: {
|
||||||
AppShell: {
|
AppShell: {
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -30,13 +30,13 @@ export const App = () => {
|
||||||
Modal: {
|
Modal: {
|
||||||
styles: {
|
styles: {
|
||||||
body: {
|
body: {
|
||||||
background: 'var(--modal-bg)',
|
background: 'var(--theme-modal-bg)',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
},
|
},
|
||||||
close: { marginRight: '0.5rem' },
|
close: { marginRight: '0.5rem' },
|
||||||
content: { borderRadius: '5px' },
|
content: { borderRadius: '5px' },
|
||||||
header: {
|
header: {
|
||||||
background: 'var(--modal-header-bg)',
|
background: 'var(--theme-modal-header-bg)',
|
||||||
paddingBottom: '1rem',
|
paddingBottom: '1rem',
|
||||||
},
|
},
|
||||||
title: { fontSize: 'medium', fontWeight: 500 },
|
title: { fontSize: 'medium', fontWeight: 500 },
|
||||||
|
|
@ -44,19 +44,8 @@ export const App = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultRadius: 'xs',
|
defaultRadius: 'xs',
|
||||||
dir: 'ltr',
|
|
||||||
focusRing: 'auto',
|
focusRing: 'auto',
|
||||||
focusRingStyles: {
|
fontFamily: 'var(--theme-content-font-family)',
|
||||||
inputStyles: () => ({
|
|
||||||
border: '1px solid var(--primary-color)',
|
|
||||||
}),
|
|
||||||
resetStyles: () => ({ outline: 'none' }),
|
|
||||||
styles: () => ({
|
|
||||||
outline: '1px solid var(--primary-color)',
|
|
||||||
outlineOffset: '-1px',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
fontFamily: 'var(--content-font-family)',
|
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
lg: '1.1rem',
|
lg: '1.1rem',
|
||||||
md: '1rem',
|
md: '1rem',
|
||||||
|
|
@ -65,8 +54,8 @@ export const App = () => {
|
||||||
xs: '0.8rem',
|
xs: '0.8rem',
|
||||||
},
|
},
|
||||||
headings: {
|
headings: {
|
||||||
fontFamily: 'var(--content-font-family)',
|
fontFamily: 'var(--theme-content-font-family)',
|
||||||
fontWeight: 700,
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
other: {},
|
other: {},
|
||||||
spacing: {
|
spacing: {
|
||||||
|
|
@ -77,8 +66,6 @@ export const App = () => {
|
||||||
xs: '0rem',
|
xs: '0rem',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
withGlobalStyles
|
|
||||||
withNormalizeCSS
|
|
||||||
>
|
>
|
||||||
<Shell />
|
<Shell />
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ export const ImageButton = () => {
|
||||||
mr={5}
|
mr={5}
|
||||||
onClick={() => toggleImage()}
|
onClick={() => toggleImage()}
|
||||||
size="xl"
|
size="xl"
|
||||||
tooltip={showImage ? 'Hide Image' : 'Show Image'}
|
tooltip={{
|
||||||
|
label: showImage ? 'Hide Image' : 'Show Image',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
{showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />}
|
{showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ export const ReconnectButton = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
$active={!connected}
|
isActive={!connected}
|
||||||
mr={5}
|
mr={5}
|
||||||
onClick={() => reconnect()}
|
onClick={() => reconnect()}
|
||||||
size="xl"
|
size="xl"
|
||||||
tooltip={connected ? 'Reconnect' : 'Not connected. Reconnect.'}
|
tooltip={{
|
||||||
|
label: connected ? 'Reconnect' : 'Not connected. Reconnect.',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<RiRestartLine size={30} />
|
<RiRestartLine size={30} />
|
||||||
|
|
|
||||||
24
src/remote/components/buttons/remote-button.module.css
Normal file
24
src/remote/components/buttons/remote-button.module.css
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
.button {
|
||||||
|
svg {
|
||||||
|
display: flex;
|
||||||
|
fill: var(--theme-colors-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
fill: var(--theme-colors-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.active {
|
||||||
|
svg {
|
||||||
|
fill: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
fill: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,53 +1,29 @@
|
||||||
import { Button, type ButtonProps as MantineButtonProps, Tooltip } from '@mantine/core';
|
import clsx from 'clsx';
|
||||||
import { forwardRef, MouseEvent, ReactNode, Ref } from 'react';
|
import { forwardRef, ReactNode, Ref } from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export interface ButtonProps extends StyledButtonProps {
|
import styles from './remote-button.module.css';
|
||||||
tooltip: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StyledButtonProps extends MantineButtonProps {
|
import { Button, ButtonProps } from '/@/shared/components/button/button';
|
||||||
$active?: boolean;
|
|
||||||
|
interface RemoteButtonProps extends ButtonProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: (e: MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
isActive?: boolean;
|
||||||
onMouseDown?: (e: MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
||||||
ref: Ref<HTMLButtonElement>;
|
ref: Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledButton = styled(Button)<StyledButtonProps>`
|
export const RemoteButton = forwardRef<HTMLButtonElement, RemoteButtonProps>(
|
||||||
svg {
|
({ children, isActive, tooltip, ...props }, ref) => {
|
||||||
display: flex;
|
|
||||||
fill: ${({ $active: active }) =>
|
|
||||||
active ? 'var(--primary-color)' : 'var(--playerbar-btn-fg)'};
|
|
||||||
stroke: var(--playerbar-btn-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--playerbar-btn-bg-hover);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: ${({ $active: active }) =>
|
|
||||||
active
|
|
||||||
? 'var(--primary-color) !important'
|
|
||||||
: 'var(--playerbar-btn-fg-hover) !important'};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const RemoteButton = forwardRef<HTMLButtonElement, any>(
|
|
||||||
({ children, tooltip, ...props }: any, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Button
|
||||||
label={tooltip}
|
className={clsx(styles.button, {
|
||||||
withinPortal
|
[styles.active]: isActive,
|
||||||
|
})}
|
||||||
|
tooltip={tooltip}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
<StyledButton
|
{children}
|
||||||
{...props}
|
</Button>
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { RiMoonLine, RiSunLine } from 'react-icons/ri';
|
||||||
|
|
||||||
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
||||||
import { useIsDark, useToggleDark } from '/@/remote/store';
|
import { useIsDark, useToggleDark } from '/@/remote/store';
|
||||||
import { AppTheme } from '/@/shared/types/domain-types';
|
import { AppTheme } from '/@/shared/themes/app-theme-types';
|
||||||
|
|
||||||
export const ThemeButton = () => {
|
export const ThemeButton = () => {
|
||||||
const isDark = useIsDark();
|
const isDark = useIsDark();
|
||||||
|
|
@ -19,7 +19,9 @@ export const ThemeButton = () => {
|
||||||
mr={5}
|
mr={5}
|
||||||
onClick={() => toggleDark()}
|
onClick={() => toggleDark()}
|
||||||
size="xl"
|
size="xl"
|
||||||
tooltip="Toggle Theme"
|
tooltip={{
|
||||||
|
label: 'Toggle Theme',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
{isDark ? <RiSunLine size={30} /> : <RiMoonLine size={30} />}
|
{isDark ? <RiSunLine size={30} /> : <RiMoonLine size={30} />}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Group, Image, Rating, Text, Title, Tooltip } from '@mantine/core';
|
import { Image, Title } from '@mantine/core';
|
||||||
import formatDuration from 'format-duration';
|
import formatDuration from 'format-duration';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
@ -17,6 +17,10 @@ import {
|
||||||
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
||||||
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
|
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
|
||||||
import { useInfo, useSend, useShowImage } from '/@/remote/store';
|
import { useInfo, useSend, useShowImage } from '/@/remote/store';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const RemoteContainer = () => {
|
export const RemoteContainer = () => {
|
||||||
|
|
@ -44,7 +48,7 @@ export const RemoteContainer = () => {
|
||||||
<Title order={2}>Album: {song.album}</Title>
|
<Title order={2}>Album: {song.album}</Title>
|
||||||
<Title order={2}>Artist: {song.artistName}</Title>
|
<Title order={2}>Artist: {song.artistName}</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Group position="apart">
|
<Group justify="space-between">
|
||||||
<Title order={3}>Duration: {formatDuration(song.duration)}</Title>
|
<Title order={3}>Duration: {formatDuration(song.duration)}</Title>
|
||||||
{song.releaseDate && (
|
{song.releaseDate && (
|
||||||
<Title order={3}>
|
<Title order={3}>
|
||||||
|
|
@ -56,13 +60,15 @@ export const RemoteContainer = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Group
|
<Group
|
||||||
|
gap={0}
|
||||||
grow
|
grow
|
||||||
spacing={0}
|
|
||||||
>
|
>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
disabled={!id}
|
disabled={!id}
|
||||||
onClick={() => send({ event: 'previous' })}
|
onClick={() => send({ event: 'previous' })}
|
||||||
tooltip="Previous track"
|
tooltip={{
|
||||||
|
label: 'Previous track',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<RiSkipBackFill size={25} />
|
<RiSkipBackFill size={25} />
|
||||||
|
|
@ -76,7 +82,9 @@ export const RemoteContainer = () => {
|
||||||
send({ event: 'play' });
|
send({ event: 'play' });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tooltip={id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
|
tooltip={{
|
||||||
|
label: id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
{id && status === PlayerStatus.PLAYING ? (
|
{id && status === PlayerStatus.PLAYING ? (
|
||||||
|
|
@ -88,34 +96,40 @@ export const RemoteContainer = () => {
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
disabled={!id}
|
disabled={!id}
|
||||||
onClick={() => send({ event: 'next' })}
|
onClick={() => send({ event: 'next' })}
|
||||||
tooltip="Next track"
|
tooltip={{
|
||||||
|
label: 'Next track',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<RiSkipForwardFill size={25} />
|
<RiSkipForwardFill size={25} />
|
||||||
</RemoteButton>
|
</RemoteButton>
|
||||||
</Group>
|
</Group>
|
||||||
<Group
|
<Group
|
||||||
|
gap={0}
|
||||||
grow
|
grow
|
||||||
spacing={0}
|
|
||||||
>
|
>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
$active={shuffle || false}
|
isActive={shuffle || false}
|
||||||
onClick={() => send({ event: 'shuffle' })}
|
onClick={() => send({ event: 'shuffle' })}
|
||||||
tooltip={shuffle ? 'Shuffle tracks' : 'Shuffle disabled'}
|
tooltip={{
|
||||||
|
label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<RiShuffleFill size={25} />
|
<RiShuffleFill size={25} />
|
||||||
</RemoteButton>
|
</RemoteButton>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
$active={repeat !== undefined && repeat !== PlayerRepeat.NONE}
|
isActive={repeat !== undefined && repeat !== PlayerRepeat.NONE}
|
||||||
onClick={() => send({ event: 'repeat' })}
|
onClick={() => send({ event: 'repeat' })}
|
||||||
tooltip={`Repeat ${
|
tooltip={{
|
||||||
repeat === PlayerRepeat.ONE
|
label: `Repeat ${
|
||||||
? 'One'
|
repeat === PlayerRepeat.ONE
|
||||||
: repeat === PlayerRepeat.ALL
|
? 'One'
|
||||||
? 'all'
|
: repeat === PlayerRepeat.ALL
|
||||||
: 'none'
|
? 'all'
|
||||||
}`}
|
: 'none'
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
{repeat === undefined || repeat === PlayerRepeat.ONE ? (
|
{repeat === undefined || repeat === PlayerRepeat.ONE ? (
|
||||||
|
|
@ -125,14 +139,16 @@ export const RemoteContainer = () => {
|
||||||
)}
|
)}
|
||||||
</RemoteButton>
|
</RemoteButton>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
$active={song?.userFavorite}
|
|
||||||
disabled={!id}
|
disabled={!id}
|
||||||
|
isActive={song?.userFavorite}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
send({ event: 'favorite', favorite: !song.userFavorite, id });
|
send({ event: 'favorite', favorite: !song.userFavorite, id });
|
||||||
}}
|
}}
|
||||||
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
|
tooltip={{
|
||||||
|
label: song?.userFavorite ? 'Unfavorite' : 'Favorite',
|
||||||
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<RiHeartLine size={25} />
|
<RiHeartLine size={25} />
|
||||||
|
|
@ -146,7 +162,7 @@ export const RemoteContainer = () => {
|
||||||
<Rating
|
<Rating
|
||||||
onChange={debouncedSetRating}
|
onChange={debouncedSetRating}
|
||||||
onDoubleClick={() => debouncedSetRating(0)}
|
onDoubleClick={() => debouncedSetRating(0)}
|
||||||
sx={{ margin: 'auto' }}
|
style={{ margin: 'auto' }}
|
||||||
value={song.userRating ?? 0}
|
value={song.userRating ?? 0}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -169,8 +185,8 @@ export const RemoteContainer = () => {
|
||||||
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
||||||
rightLabel={
|
rightLabel={
|
||||||
<Text
|
<Text
|
||||||
|
fw={600}
|
||||||
size="xs"
|
size="xs"
|
||||||
weight={600}
|
|
||||||
>
|
>
|
||||||
{volume ?? 0}
|
{volume ?? 0}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,4 @@
|
||||||
import {
|
import { AppShell, Container, Flex, Grid, Image, Skeleton, Title } from '@mantine/core';
|
||||||
AppShell,
|
|
||||||
Container,
|
|
||||||
Flex,
|
|
||||||
Grid,
|
|
||||||
Header,
|
|
||||||
Image,
|
|
||||||
MediaQuery,
|
|
||||||
Skeleton,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core';
|
|
||||||
|
|
||||||
import { ImageButton } from '/@/remote/components/buttons/image-button';
|
import { ImageButton } from '/@/remote/components/buttons/image-button';
|
||||||
import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button';
|
import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button';
|
||||||
|
|
@ -20,47 +10,35 @@ export const Shell = () => {
|
||||||
const connected = useConnected();
|
const connected = useConnected();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell padding="md">
|
||||||
header={
|
<AppShell.Header>
|
||||||
<Header height={60}>
|
<Grid>
|
||||||
<Grid>
|
<Grid.Col span="auto">
|
||||||
<Grid.Col span="auto">
|
<div>
|
||||||
<div>
|
<Image
|
||||||
<Image
|
fit="contain"
|
||||||
fit="contain"
|
height={60}
|
||||||
height={60}
|
src="/favicon.ico"
|
||||||
src="/favicon.ico"
|
width={60}
|
||||||
width={60}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</Grid.Col>
|
||||||
</Grid.Col>
|
<Grid.Col hiddenFrom="md">
|
||||||
<MediaQuery
|
<Title ta="center">Feishin Remote</Title>
|
||||||
smallerThan="sm"
|
</Grid.Col>
|
||||||
styles={{ display: 'none' }}
|
|
||||||
>
|
|
||||||
<Grid.Col
|
|
||||||
sm={6}
|
|
||||||
xs={0}
|
|
||||||
>
|
|
||||||
<Title ta="center">Feishin Remote</Title>
|
|
||||||
</Grid.Col>
|
|
||||||
</MediaQuery>
|
|
||||||
|
|
||||||
<Grid.Col span="auto">
|
<Grid.Col span="auto">
|
||||||
<Flex
|
<Flex
|
||||||
direction="row"
|
direction="row"
|
||||||
justify="right"
|
justify="right"
|
||||||
>
|
>
|
||||||
<ReconnectButton />
|
<ReconnectButton />
|
||||||
<ImageButton />
|
<ImageButton />
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Header>
|
</AppShell.Header>
|
||||||
}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<Container>
|
<Container>
|
||||||
{connected ? (
|
{connected ? (
|
||||||
<RemoteContainer />
|
<RemoteContainer />
|
||||||
|
|
|
||||||
21
src/remote/components/wrapped-slider.module.css
Normal file
21
src/remote/components/wrapped-slider.module.css
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
width: 95%;
|
||||||
|
height: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex: 6;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-self: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,16 @@
|
||||||
import { rem, Slider, SliderProps } from '@mantine/core';
|
import { rem, Slider, SliderProps } from '@mantine/core';
|
||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const SliderContainer = styled.div`
|
import styles from './wrapped-slider.module.css';
|
||||||
display: flex;
|
|
||||||
width: 95%;
|
|
||||||
height: 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SliderValueWrapper = styled.div<{ $position: 'left' | 'right' }>`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
align-self: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 50px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SliderWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 6;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
styles={{
|
styles={{
|
||||||
bar: {
|
bar: {
|
||||||
backgroundColor: 'var(--playerbar-slider-track-progress-bg)',
|
|
||||||
transition: 'background-color 0.2s ease',
|
transition: 'background-color 0.2s ease',
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
backgroundColor: 'var(--tooltip-bg)',
|
|
||||||
color: 'var(--tooltip-fg)',
|
|
||||||
fontSize: '1.1rem',
|
fontSize: '1.1rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
padding: '0 1rem',
|
padding: '0 1rem',
|
||||||
|
|
@ -59,7 +35,6 @@ const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||||
},
|
},
|
||||||
track: {
|
track: {
|
||||||
'&::before': {
|
'&::before': {
|
||||||
backgroundColor: 'var(--playerbar-slider-track-bg)',
|
|
||||||
right: 'calc(0.1rem * -1)',
|
right: 'calc(0.1rem * -1)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -84,9 +59,9 @@ export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
|
||||||
const [seek, setSeek] = useState(0);
|
const [seek, setSeek] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderContainer>
|
<div className={styles.container}>
|
||||||
{leftLabel && <SliderValueWrapper $position="left">{leftLabel}</SliderValueWrapper>}
|
{leftLabel && <div className={styles.valueWrapper}>{leftLabel}</div>}
|
||||||
<SliderWrapper>
|
<div className={styles.wrapper}>
|
||||||
<PlayerbarSlider
|
<PlayerbarSlider
|
||||||
{...props}
|
{...props}
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -102,8 +77,8 @@ export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: Wrappe
|
||||||
value={!isSeeking ? (value ?? 0) : seek}
|
value={!isSeeking ? (value ?? 0) : seek}
|
||||||
w="100%"
|
w="100%"
|
||||||
/>
|
/>
|
||||||
</SliderWrapper>
|
</div>
|
||||||
{rightLabel && <SliderValueWrapper $position="right">{rightLabel}</SliderValueWrapper>}
|
{rightLabel && <div className={styles.valueWrapper}>{rightLabel}</div>}
|
||||||
</SliderContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
|
|
||||||
|
|
||||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import { create } from 'zustand';
|
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
||||||
|
|
||||||
export interface SettingsSlice extends SettingsState {
|
export interface SettingsSlice extends SettingsState {
|
||||||
|
|
@ -36,55 +34,7 @@ const initialState: SettingsState = {
|
||||||
showImage: true,
|
showImage: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NotificationProps extends MantineNotificationProps {
|
export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||||
type?: 'error' | 'warning';
|
|
||||||
}
|
|
||||||
|
|
||||||
const showToast = ({ type, ...props }: NotificationProps) => {
|
|
||||||
const color = type === 'warning' ? 'var(--warning-color)' : 'var(--danger-color)';
|
|
||||||
|
|
||||||
const defaultTitle = type === 'warning' ? 'Warning' : 'Error';
|
|
||||||
|
|
||||||
const defaultDuration = type === 'error' ? 2000 : 1000;
|
|
||||||
|
|
||||||
return showNotification({
|
|
||||||
autoClose: defaultDuration,
|
|
||||||
styles: () => ({
|
|
||||||
closeButton: {
|
|
||||||
'&:hover': {
|
|
||||||
background: 'transparent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
color: 'var(--toast-description-fg)',
|
|
||||||
fontSize: '1rem',
|
|
||||||
},
|
|
||||||
loader: {
|
|
||||||
margin: '1rem',
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
'&::before': { backgroundColor: color },
|
|
||||||
background: 'var(--toast-bg)',
|
|
||||||
border: '2px solid var(--generic-border-color)',
|
|
||||||
bottom: '90px',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
color: 'var(--toast-title-fg)',
|
|
||||||
fontSize: '1.3rem',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
title: defaultTitle,
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toast = {
|
|
||||||
error: (props: NotificationProps) => showToast({ type: 'error', ...props }),
|
|
||||||
hide: hideNotification,
|
|
||||||
warn: (props: NotificationProps) => showToast({ type: 'warning', ...props }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRemoteStore = create<SettingsSlice>()(
|
|
||||||
persist(
|
persist(
|
||||||
devtools(
|
devtools(
|
||||||
immer((set, get) => ({
|
immer((set, get) => ({
|
||||||
|
|
|
||||||
112
src/remote/styles/global.css
Normal file
112
src/remote/styles/global.css
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
@import url('../../renderer/styles/ag-grid.css');
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--theme-content-font-family);
|
||||||
|
font-size: var(--theme-root-font-size);
|
||||||
|
color: var(--theme-content-text-color);
|
||||||
|
user-select: none;
|
||||||
|
background: var(--theme-content-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (width < 640px) {
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
|
||||||
|
text-size-adjust: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ::-webkit-scrollbar-corner {
|
||||||
|
background: var(--theme-scrollbar-track-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--theme-scrollbar-track-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--theme-scrollbar-handle-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--theme-scrollbar-handle-hover-background);
|
||||||
|
} */
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-scrollbar {
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar {
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
@use '../../renderer/themes/default.scss';
|
|
||||||
@use '../../renderer/themes/dark.scss';
|
|
||||||
@use '../../renderer/themes/light.scss';
|
|
||||||
@use '../../renderer/styles/ag-grid.scss';
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
color: var(--content-text-color);
|
|
||||||
background: var(--content-bg);
|
|
||||||
font-family: var(--content-font-family);
|
|
||||||
font-size: var(--root-font-size);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 639px) {
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
|
||||||
background: var(--scrollbar-track-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: var(--scrollbar-track-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--scrollbar-thumb-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--scrollbar-thumb-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-scrollbar {
|
|
||||||
overflow-y: overlay !important;
|
|
||||||
overflow-x: overlay !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-scrollbar {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: transparent transparent;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none; /* Safari and Chrome */
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mantine-ScrollArea-thumb[data-state='visible'] {
|
|
||||||
animation: fadeIn 0.3s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mantine-ScrollArea-scrollbar[data-state='hidden'] {
|
|
||||||
animation: fadeOut 0.2s forwards;
|
|
||||||
}
|
|
||||||
|
|
@ -2,8 +2,8 @@ import i18n from '/@/i18n/i18n';
|
||||||
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||||
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import {
|
import {
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
ControllerEndpoint,
|
ControllerEndpoint,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import qs from 'qs';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||||
import { toast } from '/@/renderer/components';
|
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||||
import { resultWithHeaders } from '/@/shared/api/utils';
|
import { resultWithHeaders } from '/@/shared/api/utils';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import qs from 'qs';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
|
||||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { toast } from '/@/renderer/components';
|
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerListItem } from '/@/shared/types/types';
|
import { ServerListItem } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,21 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod
|
||||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||||
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { initSimpleImg } from 'react-simple-img';
|
import '@mantine/core/styles.css';
|
||||||
|
import '@mantine/notifications/styles.css';
|
||||||
|
import '@mantine/dates/styles.css';
|
||||||
|
|
||||||
import './styles/global.scss';
|
import './styles/global.css';
|
||||||
|
|
||||||
import '@ag-grid-community/styles/ag-grid.css';
|
import '@ag-grid-community/styles/ag-grid.css';
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
|
|
||||||
|
import './styles/overlayscrollbars.css';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { toast } from '/@/renderer/components';
|
|
||||||
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
||||||
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
||||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||||
|
|
@ -20,7 +24,6 @@ import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-co
|
||||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||||
import { useTheme } from '/@/renderer/hooks';
|
|
||||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||||
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
|
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
|
|
@ -34,71 +37,33 @@ import {
|
||||||
useRemoteSettings,
|
useRemoteSettings,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
|
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||||
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
import { PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
|
||||||
|
|
||||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||||
|
|
||||||
initSimpleImg({ threshold: 0.05 }, true);
|
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
const remote = isElectron() ? window.api.remote : null;
|
const remote = isElectron() ? window.api.remote : null;
|
||||||
const utils = isElectron() ? window.api.utils : null;
|
const utils = isElectron() ? window.api.utils : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const theme = useTheme();
|
const { mode, theme } = useAppTheme();
|
||||||
const accent = useSettingsStore((store) => store.general.accent);
|
|
||||||
const language = useSettingsStore((store) => store.general.language);
|
const language = useSettingsStore((store) => store.general.language);
|
||||||
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
|
|
||||||
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
|
||||||
const { content, enabled } = useCssSettings();
|
const { content, enabled } = useCssSettings();
|
||||||
const { type: playbackType } = usePlaybackSettings();
|
const { type: playbackType } = usePlaybackSettings();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||||
const { clearQueue, restoreQueue } = useQueueControls();
|
const { clearQueue, restoreQueue } = useQueueControls();
|
||||||
const remoteSettings = useRemoteSettings();
|
const remoteSettings = useRemoteSettings();
|
||||||
const textStyleRef = useRef<HTMLStyleElement | null>(null);
|
|
||||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||||
useDiscordRpc();
|
useDiscordRpc();
|
||||||
useServerVersion();
|
useServerVersion();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (type === FontType.SYSTEM && system) {
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.setProperty('--content-font-family', 'dynamic-font');
|
|
||||||
|
|
||||||
if (!textStyleRef.current) {
|
|
||||||
textStyleRef.current = document.createElement('style');
|
|
||||||
document.body.appendChild(textStyleRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
textStyleRef.current.textContent = `
|
|
||||||
@font-face {
|
|
||||||
font-family: "dynamic-font";
|
|
||||||
src: local("${system}");
|
|
||||||
}`;
|
|
||||||
} else if (type === FontType.CUSTOM && custom) {
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.setProperty('--content-font-family', 'dynamic-font');
|
|
||||||
|
|
||||||
if (!textStyleRef.current) {
|
|
||||||
textStyleRef.current = document.createElement('style');
|
|
||||||
document.body.appendChild(textStyleRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
textStyleRef.current.textContent = `
|
|
||||||
@font-face {
|
|
||||||
font-family: "dynamic-font";
|
|
||||||
src: url("feishin://${custom}");
|
|
||||||
}`;
|
|
||||||
} else {
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.setProperty('--content-font-family', builtIn);
|
|
||||||
}
|
|
||||||
}, [builtIn, custom, system, type]);
|
|
||||||
|
|
||||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -121,16 +86,6 @@ export const App = () => {
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [content, enabled]);
|
}, [content, enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.setProperty('--primary-color', accent);
|
|
||||||
}, [accent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
root.style.setProperty('--image-fit', nativeImageAspect ? 'contain' : 'cover');
|
|
||||||
}, [nativeImageAspect]);
|
|
||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return { handlePlayQueueAdd };
|
return { handlePlayQueueAdd };
|
||||||
}, [handlePlayQueueAdd]);
|
}, [handlePlayQueueAdd]);
|
||||||
|
|
@ -237,59 +192,14 @@ export const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
theme={{
|
defaultColorScheme={mode as 'dark' | 'light'}
|
||||||
colorScheme: theme as 'dark' | 'light',
|
theme={theme}
|
||||||
components: {
|
|
||||||
Modal: {
|
|
||||||
styles: {
|
|
||||||
body: { background: 'var(--modal-bg)', padding: '1rem !important' },
|
|
||||||
close: { marginRight: '0.5rem' },
|
|
||||||
content: { borderRadius: '5px' },
|
|
||||||
header: {
|
|
||||||
background: 'var(--modal-header-bg)',
|
|
||||||
paddingBottom: '1rem',
|
|
||||||
},
|
|
||||||
title: { fontSize: 'medium', fontWeight: 500 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultRadius: 'xs',
|
|
||||||
dir: 'ltr',
|
|
||||||
focusRing: 'auto',
|
|
||||||
focusRingStyles: {
|
|
||||||
inputStyles: () => ({
|
|
||||||
border: '1px solid var(--primary-color)',
|
|
||||||
}),
|
|
||||||
resetStyles: () => ({ outline: 'none' }),
|
|
||||||
styles: () => ({
|
|
||||||
outline: '1px solid var(--primary-color)',
|
|
||||||
outlineOffset: '-1px',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
fontFamily: 'var(--content-font-family)',
|
|
||||||
fontSizes: {
|
|
||||||
lg: '1.1rem',
|
|
||||||
md: '1rem',
|
|
||||||
sm: '0.9rem',
|
|
||||||
xl: '1.5rem',
|
|
||||||
xs: '0.8rem',
|
|
||||||
},
|
|
||||||
headings: {
|
|
||||||
fontFamily: 'var(--content-font-family)',
|
|
||||||
fontWeight: 700,
|
|
||||||
},
|
|
||||||
other: {},
|
|
||||||
spacing: {
|
|
||||||
lg: '2rem',
|
|
||||||
md: '1rem',
|
|
||||||
sm: '0.5rem',
|
|
||||||
xl: '4rem',
|
|
||||||
xs: '0rem',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
withGlobalStyles
|
|
||||||
withNormalizeCSS
|
|
||||||
>
|
>
|
||||||
|
<Notifications
|
||||||
|
containerWidth="300px"
|
||||||
|
position="bottom-center"
|
||||||
|
zIndex={5}
|
||||||
|
/>
|
||||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||||
<ContextMenuProvider>
|
<ContextMenuProvider>
|
||||||
<WebAudioContext.Provider value={webAudioProvider}>
|
<WebAudioContext.Provider value={webAudioProvider}>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import type { AccordionProps as MantineAccordionProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Accordion as MantineAccordion } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type AccordionProps = MantineAccordionProps;
|
|
||||||
|
|
||||||
const StyledAccordion = styled(MantineAccordion)`
|
|
||||||
& .mantine-Accordion-panel {
|
|
||||||
background: var(--paper-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mantine-Accordion-control {
|
|
||||||
background: var(--paper-bg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Accordion = ({ children, ...props }: AccordionProps) => {
|
|
||||||
return <StyledAccordion {...props}>{children}</StyledAccordion>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Accordion.Control = StyledAccordion.Control;
|
|
||||||
Accordion.Item = StyledAccordion.Item;
|
|
||||||
Accordion.Panel = StyledAccordion.Panel;
|
|
||||||
|
|
@ -19,10 +19,10 @@ import {
|
||||||
crossfadeHandler,
|
crossfadeHandler,
|
||||||
gaplessHandler,
|
gaplessHandler,
|
||||||
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
||||||
import { toast } from '/@/renderer/components/toast';
|
|
||||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||||
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
||||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
|
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
export type AudioPlayerProgress = {
|
export type AudioPlayerProgress = {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import type { BadgeProps as MantineBadgeProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { createPolymorphicComponent, Badge as MantineBadge } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export type BadgeProps = MantineBadgeProps;
|
|
||||||
|
|
||||||
const StyledBadge = styled(MantineBadge)<BadgeProps>`
|
|
||||||
border-radius: var(--badge-radius);
|
|
||||||
|
|
||||||
.mantine-Badge-root {
|
|
||||||
color: var(--badge-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mantine-Badge-inner {
|
|
||||||
color: var(--badge-fg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const _Badge = ({ children, ...props }: BadgeProps) => {
|
|
||||||
return (
|
|
||||||
<StyledBadge
|
|
||||||
radius="md"
|
|
||||||
size="sm"
|
|
||||||
styles={{
|
|
||||||
root: { background: 'var(--badge-bg)' },
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledBadge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Badge = createPolymorphicComponent<'button', BadgeProps>(_Badge);
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
import type { ButtonProps as MantineButtonProps, TooltipProps } from '@mantine/core';
|
|
||||||
import type { Ref } from 'react';
|
|
||||||
|
|
||||||
import { createPolymorphicComponent, Button as MantineButton } from '@mantine/core';
|
|
||||||
import { useTimeout } from '@mantine/hooks';
|
|
||||||
import React, { forwardRef, useCallback, useRef, useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { Spinner } from '/@/renderer/components/spinner';
|
|
||||||
import { Tooltip } from '/@/renderer/components/tooltip';
|
|
||||||
|
|
||||||
export interface ButtonProps extends MantineButtonProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
loading?: boolean;
|
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
||||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
||||||
tooltip?: Omit<TooltipProps, 'children'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StyledButtonProps extends ButtonProps {
|
|
||||||
ref: Ref<HTMLButtonElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledButton = styled(MantineButton)<StyledButtonProps>`
|
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
|
||||||
border: ${(props) => `var(--btn-${props.variant}-border)`};
|
|
||||||
border-radius: ${(props) => `var(--btn-${props.variant}-radius)`};
|
|
||||||
transition:
|
|
||||||
background 0.2s ease-in-out,
|
|
||||||
color 0.2s ease-in-out,
|
|
||||||
border 0.2s ease-in-out;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: ${(props) => `var(--btn-${props.variant}-fg)`};
|
|
||||||
transition: fill 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
|
||||||
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([data-disabled])&:hover {
|
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg) !important`};
|
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
|
||||||
filter: brightness(85%);
|
|
||||||
border: ${(props) => `var(--btn-${props.variant}-border-hover)`};
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([data-disabled])&:focus-visible {
|
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
|
||||||
filter: brightness(85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Button-centerLoader {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Button-leftIcon {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mantine-Button-rightIcon {
|
|
||||||
display: flex;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ButtonChildWrapper = styled.span<{ $loading?: boolean }>`
|
|
||||||
color: ${(props) => props.$loading && 'transparent !important'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SpinnerWrapper = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate3d(-50%, -50%, 0);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ children, tooltip, ...props }: ButtonProps, ref) => {
|
|
||||||
if (tooltip) {
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
withinPortal
|
|
||||||
{...tooltip}
|
|
||||||
>
|
|
||||||
<StyledButton
|
|
||||||
loaderPosition="center"
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
|
||||||
{props.loading && (
|
|
||||||
<SpinnerWrapper>
|
|
||||||
<Spinner />
|
|
||||||
</SpinnerWrapper>
|
|
||||||
)}
|
|
||||||
</StyledButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledButton
|
|
||||||
loaderPosition="center"
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
|
||||||
{props.loading && (
|
|
||||||
<SpinnerWrapper>
|
|
||||||
<Spinner />
|
|
||||||
</SpinnerWrapper>
|
|
||||||
)}
|
|
||||||
</StyledButton>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Button = createPolymorphicComponent<'button', ButtonProps>(_Button);
|
|
||||||
|
|
||||||
interface HoldButtonProps extends ButtonProps {
|
|
||||||
timeoutProps: {
|
|
||||||
callback: () => void;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
|
|
||||||
const [, setTimeoutRemaining] = useState(timeoutProps.duration);
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
|
||||||
const intervalRef = useRef(0);
|
|
||||||
|
|
||||||
const callback = () => {
|
|
||||||
timeoutProps.callback();
|
|
||||||
setTimeoutRemaining(timeoutProps.duration);
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
setIsRunning(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { clear, start } = useTimeout(callback, timeoutProps.duration);
|
|
||||||
|
|
||||||
const startTimeout = useCallback(() => {
|
|
||||||
if (isRunning) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
setIsRunning(false);
|
|
||||||
clear();
|
|
||||||
} else {
|
|
||||||
setIsRunning(true);
|
|
||||||
start();
|
|
||||||
|
|
||||||
const intervalId = window.setInterval(() => {
|
|
||||||
setTimeoutRemaining((prev) => prev - 100);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
intervalRef.current = intervalId;
|
|
||||||
}
|
|
||||||
}, [clear, isRunning, start]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={startTimeout}
|
|
||||||
sx={{ color: 'var(--danger-color)' }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{isRunning ? 'Cancel' : props.children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
import type { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
import { Center } from '@mantine/core';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { RiAlbumFill } from 'react-icons/ri';
|
|
||||||
import { generatePath, useNavigate } from 'react-router';
|
|
||||||
import { SimpleImg } from 'react-simple-img';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { CardControls } from '/@/renderer/components/card/card-controls';
|
|
||||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
const CardWrapper = styled.div<{
|
|
||||||
link?: boolean;
|
|
||||||
}>`
|
|
||||||
padding: 1rem;
|
|
||||||
cursor: ${({ link }) => link && 'pointer'};
|
|
||||||
background: var(--card-default-bg);
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
transition:
|
|
||||||
border 0.2s ease-in-out,
|
|
||||||
background 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--card-default-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover div {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover * {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 1px solid #fff;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCard = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageSection = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: '';
|
|
||||||
user-select: none;
|
|
||||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 20%);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ControlsContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 50;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailSection = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Row = styled.div<{ $secondary?: boolean }>`
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
padding: 0 0.2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
|
||||||
controls: {
|
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
|
||||||
itemType: LibraryItem;
|
|
||||||
playButtonBehavior: Play;
|
|
||||||
route: CardRoute;
|
|
||||||
};
|
|
||||||
data: any;
|
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
|
||||||
loading?: boolean;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AlbumCard = ({
|
|
||||||
controls,
|
|
||||||
data,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
loading,
|
|
||||||
size,
|
|
||||||
}: BaseGridCardProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { cardRows, itemType, route } = controls;
|
|
||||||
|
|
||||||
const handleNavigate = useCallback(() => {
|
|
||||||
navigate(
|
|
||||||
generatePath(
|
|
||||||
route.route as string,
|
|
||||||
route.slugs?.reduce((acc, slug) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, [data, navigate, route.route, route.slugs]);
|
|
||||||
|
|
||||||
if (!loading) {
|
|
||||||
return (
|
|
||||||
<CardWrapper
|
|
||||||
link
|
|
||||||
onClick={handleNavigate}
|
|
||||||
>
|
|
||||||
<StyledCard>
|
|
||||||
<ImageSection>
|
|
||||||
{data?.imageUrl ? (
|
|
||||||
<Image
|
|
||||||
animationDuration={0.3}
|
|
||||||
height={size}
|
|
||||||
imgStyle={{ objectFit: 'cover' }}
|
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
|
||||||
src={data?.imageUrl}
|
|
||||||
width={size}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-default-radius)',
|
|
||||||
height: `${size}px`,
|
|
||||||
width: `${size}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiAlbumFill
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<ControlsContainer>
|
|
||||||
<CardControls
|
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
|
||||||
itemData={data}
|
|
||||||
itemType={itemType}
|
|
||||||
/>
|
|
||||||
</ControlsContainer>
|
|
||||||
</ImageSection>
|
|
||||||
<DetailSection>
|
|
||||||
<CardRows
|
|
||||||
data={data}
|
|
||||||
rows={cardRows}
|
|
||||||
/>
|
|
||||||
</DetailSection>
|
|
||||||
</StyledCard>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardWrapper>
|
|
||||||
<StyledCard style={{ alignItems: 'center', display: 'flex' }}>
|
|
||||||
<Skeleton
|
|
||||||
height={size}
|
|
||||||
radius="sm"
|
|
||||||
visible
|
|
||||||
width={size}
|
|
||||||
>
|
|
||||||
<ImageSection />
|
|
||||||
</Skeleton>
|
|
||||||
<DetailSection style={{ width: '100%' }}>
|
|
||||||
{(cardRows || []).map((_row: CardRow<Album>, index: number) => (
|
|
||||||
<Skeleton
|
|
||||||
height={15}
|
|
||||||
key={`skeleton-${data?.id}-${index}`}
|
|
||||||
my={3}
|
|
||||||
radius="md"
|
|
||||||
visible
|
|
||||||
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
|
||||||
>
|
|
||||||
<Row />
|
|
||||||
</Skeleton>
|
|
||||||
))}
|
|
||||||
</DetailSection>
|
|
||||||
</StyledCard>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
71
src/renderer/components/card/card-controls.module.css
Normal file
71
src/renderer/components/card/card-controls.module.css
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
.play-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: rgb(255 255 255);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
transition: scale 0.2s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: rgb(0 0 0);
|
||||||
|
stroke: rgb(0 0 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
transition: scale 0.2s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-controls-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-row {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-wrapper {
|
||||||
|
svg {
|
||||||
|
fill: var(--theme-colors-primary-filled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,110 +1,21 @@
|
||||||
import type { PlayQueueAddOptions } from '/@/shared/types/types';
|
import type { PlayQueueAddOptions } from '/@/shared/types/types';
|
||||||
import type { UnstyledButtonProps } from '@mantine/core';
|
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { Group } from '@mantine/core';
|
import styles from './card-controls.module.css';
|
||||||
import React from 'react';
|
|
||||||
import { RiHeartFill, RiHeartLine, RiMore2Fill, RiPlayFill } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { _Button } from '/@/renderer/components/button';
|
|
||||||
import {
|
import {
|
||||||
ALBUM_CONTEXT_MENU_ITEMS,
|
ALBUM_CONTEXT_MENU_ITEMS,
|
||||||
ARTIST_CONTEXT_MENU_ITEMS,
|
ARTIST_CONTEXT_MENU_ITEMS,
|
||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
|
||||||
|
|
||||||
const PlayButton = styled.button<PlayButtonType>`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: rgb(255 255 255);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
transition: scale 0.2s linear;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: rgb(0 0 0);
|
|
||||||
stroke: rgb(0 0 0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SecondaryButton = styled(_Button)`
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
transition: scale 0.2s linear;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GridCardControlsContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ControlsRow = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% / 3);
|
|
||||||
`;
|
|
||||||
|
|
||||||
// const TopControls = styled(ControlsRow)`
|
|
||||||
// display: flex;
|
|
||||||
// align-items: flex-start;
|
|
||||||
// justify-content: space-between;
|
|
||||||
// padding: 0.5rem;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// const CenterControls = styled(ControlsRow)`
|
|
||||||
// display: flex;
|
|
||||||
// align-items: center;
|
|
||||||
// justify-content: center;
|
|
||||||
// padding: 0.5rem;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const BottomControls = styled(ControlsRow)`
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
|
||||||
svg {
|
|
||||||
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CardControls = ({
|
export const CardControls = ({
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
itemData,
|
itemData,
|
||||||
|
|
@ -134,46 +45,45 @@ export const CardControls = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCardControlsContainer>
|
<div className={styles.gridCardControlsContainer}>
|
||||||
<BottomControls>
|
<div className={styles.bottomControls}>
|
||||||
<PlayButton onClick={handlePlay}>
|
<button
|
||||||
<RiPlayFill size={25} />
|
className={styles.playButton}
|
||||||
</PlayButton>
|
onClick={handlePlay}
|
||||||
<Group spacing="xs">
|
>
|
||||||
<SecondaryButton
|
<Icon icon="mediaPlay" />
|
||||||
|
</button>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
className={styles.secondaryButton}
|
||||||
disabled
|
disabled
|
||||||
p={5}
|
p={5}
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
style={{ svg: { fill: 'white !important' } }}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
<div className={itemData?.isFavorite ? styles.favoriteWrapper : ''}>
|
||||||
{itemData?.isFavorite ? (
|
{itemData?.isFavorite ? (
|
||||||
<RiHeartFill size={20} />
|
<Icon icon="favorite" />
|
||||||
) : (
|
) : (
|
||||||
<RiHeartLine
|
<Icon icon="favorite" />
|
||||||
color="white"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</FavoriteWrapper>
|
</div>
|
||||||
</SecondaryButton>
|
</Button>
|
||||||
<SecondaryButton
|
<ActionIcon
|
||||||
onClick={(e) => {
|
className={styles.secondaryButton}
|
||||||
|
onClick={(e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleContextMenu(e, [itemData]);
|
handleContextMenu(e, [itemData]);
|
||||||
}}
|
}}
|
||||||
p={5}
|
p={5}
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
style={{ svg: { fill: 'white !important' } }}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
<RiMore2Fill
|
<Icon icon="ellipsisHorizontal" />
|
||||||
color="white"
|
</ActionIcon>
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</SecondaryButton>
|
|
||||||
</Group>
|
</Group>
|
||||||
</BottomControls>
|
</div>
|
||||||
</GridCardControlsContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
15
src/renderer/components/card/card-rows.module.css
Normal file
15
src/renderer/components/card/card-rows.module.css
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
.row {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--theme-colors-foreground);
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.secondary {
|
||||||
|
color: var(--theme-colors-foreground-muted);
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,17 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
import formatDuration from 'format-duration';
|
import formatDuration from 'format-duration';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { Text } from '/@/renderer/components/text';
|
import styles from './card-rows.module.css';
|
||||||
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/shared/types/domain-types';
|
import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/shared/types/domain-types';
|
||||||
import { CardRow } from '/@/shared/types/types';
|
import { CardRow } from '/@/shared/types/types';
|
||||||
|
|
||||||
const Row = styled.div<{ $secondary?: boolean }>`
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
padding: 0 0.2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface CardRowsProps {
|
interface CardRowsProps {
|
||||||
data: any;
|
data: any;
|
||||||
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||||
|
|
@ -33,17 +23,19 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
{rows.map((row, index: number) => {
|
{rows.map((row, index: number) => {
|
||||||
if (row.arrayProperty && row.route) {
|
if (row.arrayProperty && row.route) {
|
||||||
return (
|
return (
|
||||||
<Row
|
<div
|
||||||
$secondary={index > 0}
|
className={clsx(styles.row, {
|
||||||
|
[styles.secondary]: index > 0,
|
||||||
|
})}
|
||||||
key={`row-${row.property}-${index}`}
|
key={`row-${row.property}-${index}`}
|
||||||
>
|
>
|
||||||
{data[row.property].map((item: any, itemIndex: number) => (
|
{data[row.property].map((item: any, itemIndex: number) => (
|
||||||
<React.Fragment key={`${data.id}-${item.id}`}>
|
<React.Fragment key={`${data.id}-${item.id}`}>
|
||||||
{itemIndex > 0 && (
|
{itemIndex > 0 && (
|
||||||
<Text
|
<Text
|
||||||
$noSelect
|
isMuted
|
||||||
$secondary
|
isNoSelect
|
||||||
sx={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
padding: '0 2px 0 1px',
|
padding: '0 2px 0 1px',
|
||||||
}}
|
}}
|
||||||
|
|
@ -52,10 +44,10 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<Text
|
<Text
|
||||||
$link
|
|
||||||
$noSelect
|
|
||||||
$secondary={index > 0}
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink
|
||||||
|
isMuted={index > 0}
|
||||||
|
isNoSelect
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
|
|
@ -79,17 +71,22 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.arrayProperty) {
|
if (row.arrayProperty) {
|
||||||
return (
|
return (
|
||||||
<Row key={`row-${row.property}`}>
|
<div
|
||||||
|
className={clsx(styles.row, {
|
||||||
|
[styles.secondary]: index > 0,
|
||||||
|
})}
|
||||||
|
key={`row-${row.property}`}
|
||||||
|
>
|
||||||
{data[row.property].map((item: any) => (
|
{data[row.property].map((item: any) => (
|
||||||
<Text
|
<Text
|
||||||
$noSelect
|
isMuted={index > 0}
|
||||||
$secondary={index > 0}
|
isNoSelect
|
||||||
key={`${data.id}-${item.id}`}
|
key={`${data.id}-${item.id}`}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
|
|
@ -98,17 +95,22 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
(row.format ? row.format(item) : item[row.arrayProperty])}
|
(row.format ? row.format(item) : item[row.arrayProperty])}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row key={`row-${row.property}`}>
|
<div
|
||||||
|
className={clsx(styles.row, {
|
||||||
|
[styles.secondary]: index > 0,
|
||||||
|
})}
|
||||||
|
key={`row-${row.property}`}
|
||||||
|
>
|
||||||
{row.route ? (
|
{row.route ? (
|
||||||
<Text
|
<Text
|
||||||
$link
|
|
||||||
$noSelect
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink
|
||||||
|
isNoSelect
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
to={generatePath(
|
to={generatePath(
|
||||||
|
|
@ -125,15 +127,15 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
$noSelect
|
isMuted={index > 0}
|
||||||
$secondary={index > 0}
|
isNoSelect
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
>
|
>
|
||||||
{data && (row.format ? row.format(data) : data[row.property])}
|
{data && (row.format ? row.format(data) : data[row.property])}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './album-card';
|
|
||||||
export * from './card-rows';
|
|
||||||
67
src/renderer/components/card/poster-card.module.css
Normal file
67
src/renderer/components/card/poster-card.module.css
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&:global(.card-controls) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--theme-card-default-bg);
|
||||||
|
border-radius: var(--theme-card-poster-radius);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
content: '';
|
||||||
|
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:global(.card-controls) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100%;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: var(--theme-image-fit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-container {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { Center, Stack } from '@mantine/core';
|
import { useState } from 'react';
|
||||||
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
import { SimpleImg } from 'react-simple-img';
|
|
||||||
import styled, { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { CardRows } from '/@/renderer/components/card';
|
import styles from './poster-card.module.css';
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
|
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||||
|
import { Image } from '/@/shared/components/image/image';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
|
@ -28,85 +29,14 @@ interface BaseGridCardProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageContainerStyles = css`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--card-default-bg);
|
|
||||||
border-radius: var(--card-poster-radius);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: '';
|
|
||||||
user-select: none;
|
|
||||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .card-controls {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageContainer = styled(Link)<{ $isFavorite?: boolean }>`
|
|
||||||
${ImageContainerStyles}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageContainerSkeleton = styled.div`
|
|
||||||
${ImageContainerStyles}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100% !important;
|
|
||||||
max-height: 100%;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: var(--image-fit);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailContainer = styled.div`
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const PosterCard = ({
|
export const PosterCard = ({
|
||||||
controls,
|
controls,
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
}: BaseGridCardProps & { uniqueId: string }) => {
|
}: BaseGridCardProps & { uniqueId: string }) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
const path = generatePath(
|
const path = generatePath(
|
||||||
controls.route.route as string,
|
controls.route.route as string,
|
||||||
|
|
@ -118,90 +48,57 @@ export const PosterCard = ({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Placeholder = RiAlbumFill;
|
|
||||||
|
|
||||||
switch (controls.itemType) {
|
|
||||||
case LibraryItem.ALBUM:
|
|
||||||
Placeholder = RiAlbumFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
|
||||||
Placeholder = RiUserVoiceFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.ARTIST:
|
|
||||||
Placeholder = RiUserVoiceFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.PLAYLIST:
|
|
||||||
Placeholder = RiPlayListFill;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Placeholder = RiAlbumFill;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PosterCardContainer key={`${uniqueId}-${data.id}`}>
|
<div
|
||||||
<ImageContainer
|
className={styles.container}
|
||||||
$isFavorite={data?.userFavorite}
|
key={`${uniqueId}-${data.id}`}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className={styles.imageContainer}
|
||||||
to={path}
|
to={path}
|
||||||
>
|
>
|
||||||
{data?.imageUrl ? (
|
<Image
|
||||||
<Image
|
className={styles.image}
|
||||||
importance="auto"
|
src={data?.imageUrl}
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
/>
|
||||||
src={data?.imageUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-default-radius)',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Placeholder
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<GridCardControls
|
<GridCardControls
|
||||||
handleFavorite={controls.handleFavorite}
|
handleFavorite={controls.handleFavorite}
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
isHovered={isHovered}
|
||||||
itemData={data}
|
itemData={data}
|
||||||
itemType={controls.itemType}
|
itemType={controls.itemType}
|
||||||
/>
|
/>
|
||||||
</ImageContainer>
|
</Link>
|
||||||
<DetailContainer>
|
<div className={styles.detailContainer}>
|
||||||
<CardRows
|
<CardRows
|
||||||
data={data}
|
data={data}
|
||||||
rows={controls.cardRows}
|
rows={controls.cardRows}
|
||||||
/>
|
/>
|
||||||
</DetailContainer>
|
</div>
|
||||||
</PosterCardContainer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
|
<div
|
||||||
<Skeleton
|
className={styles.container}
|
||||||
radius="sm"
|
key={`placeholder-${uniqueId}-${data.id}`}
|
||||||
visible
|
>
|
||||||
>
|
<div className={styles.imageContainer}>
|
||||||
<ImageContainerSkeleton />
|
<Skeleton className={styles.image} />
|
||||||
</Skeleton>
|
</div>
|
||||||
<DetailContainer>
|
<div className={styles.detailContainer}>
|
||||||
<Stack spacing="sm">
|
<Stack gap="xs">
|
||||||
{(controls?.cardRows || []).map((row, index) => (
|
{(controls?.cardRows || []).map((row, index) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height={14}
|
height={14}
|
||||||
key={`${index}-${row.arrayProperty}`}
|
key={`${index}-${row.arrayProperty}`}
|
||||||
radius="sm"
|
|
||||||
visible
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</DetailContainer>
|
</div>
|
||||||
</PosterCardContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { CheckboxProps, Checkbox as MantineCheckbox } from '@mantine/core';
|
|
||||||
import { forwardRef } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledCheckbox = styled(MantineCheckbox)`
|
|
||||||
& .mantine-Checkbox-input {
|
|
||||||
background-color: var(--input-bg);
|
|
||||||
|
|
||||||
&:checked {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(:checked) {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|
||||||
({ ...props }: CheckboxProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledCheckbox
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
35
src/renderer/components/context-menu/context-menu.module.css
Normal file
35
src/renderer/components/context-menu/context-menu.module.css
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: var(--theme-spacing-xs);
|
||||||
|
background: var(--theme-colors-surface);
|
||||||
|
border: 1px solid var(--theme-colors-border);
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-button {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--theme-spacing-sm);
|
||||||
|
font-family: var(--theme-content-font-family);
|
||||||
|
font-size: var(--theme-font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--theme-colors-surface-foreground);
|
||||||
|
text-align: left;
|
||||||
|
cursor: default;
|
||||||
|
background: var(--theme-colors-surface);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: lighten(var(--theme-colors-surface), 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: transparent;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
margin-right: 3rem;
|
||||||
|
}
|
||||||
91
src/renderer/components/context-menu/context-menu.tsx
Normal file
91
src/renderer/components/context-menu/context-menu.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { motion, Variants } from 'motion/react';
|
||||||
|
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
|
||||||
|
|
||||||
|
import styles from './context-menu.module.css';
|
||||||
|
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
children: ReactNode;
|
||||||
|
maxWidth?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMenuButton = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<'button'> & {
|
||||||
|
leftIcon?: ReactNode;
|
||||||
|
rightIcon?: ReactNode;
|
||||||
|
},
|
||||||
|
ref: any,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={styles.contextMenuButton}
|
||||||
|
disabled={props.disabled}
|
||||||
|
key={props.key}
|
||||||
|
onClick={props.onClick}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
className={styles.left}
|
||||||
|
gap="md"
|
||||||
|
>
|
||||||
|
{leftIcon}
|
||||||
|
{children}
|
||||||
|
</Group>
|
||||||
|
{rightIcon}
|
||||||
|
</Group>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
closed: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContextMenu = forwardRef(
|
||||||
|
({ children, maxWidth, minWidth, xPos, yPos }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate="open"
|
||||||
|
className={styles.container}
|
||||||
|
initial="closed"
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
left: xPos,
|
||||||
|
maxWidth,
|
||||||
|
minWidth,
|
||||||
|
top: yPos,
|
||||||
|
}}
|
||||||
|
variants={variants}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
import { Box, Flex, Group, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
|
|
||||||
import { motion, Variants } from 'framer-motion';
|
|
||||||
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
children: ReactNode;
|
|
||||||
maxWidth?: number;
|
|
||||||
minWidth?: number;
|
|
||||||
xPos: number;
|
|
||||||
yPos: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextMenuContainer = styled(motion.div)<Omit<ContextMenuProps, 'children'>>`
|
|
||||||
position: absolute;
|
|
||||||
top: ${({ yPos }) => yPos}px !important;
|
|
||||||
left: ${({ xPos }) => xPos}px !important;
|
|
||||||
z-index: 1000;
|
|
||||||
min-width: ${({ minWidth }) => minWidth}px;
|
|
||||||
max-width: ${({ maxWidth }) => maxWidth}px;
|
|
||||||
background: var(--dropdown-menu-bg);
|
|
||||||
border-radius: var(--dropdown-menu-border-radius);
|
|
||||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
|
||||||
|
|
||||||
button:first-child {
|
|
||||||
border-top-left-radius: var(--dropdown-menu-border-radius);
|
|
||||||
border-top-right-radius: var(--dropdown-menu-border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:last-child {
|
|
||||||
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
|
||||||
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledContextMenuButton = styled(UnstyledButton)`
|
|
||||||
padding: var(--dropdown-menu-item-padding);
|
|
||||||
font-family: var(--content-font-family);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--dropdown-menu-fg);
|
|
||||||
text-align: left;
|
|
||||||
cursor: default;
|
|
||||||
background: var(--dropdown-menu-bg);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
& .mantine-Button-inner {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--dropdown-menu-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background: transparent;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ContextMenuButton = forwardRef(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
leftIcon,
|
|
||||||
rightIcon,
|
|
||||||
...props
|
|
||||||
}: ComponentPropsWithoutRef<'button'> &
|
|
||||||
UnstyledButtonProps & {
|
|
||||||
leftIcon?: ReactNode;
|
|
||||||
rightIcon?: ReactNode;
|
|
||||||
},
|
|
||||||
ref: any,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<StyledContextMenuButton
|
|
||||||
{...props}
|
|
||||||
as="button"
|
|
||||||
disabled={props.disabled}
|
|
||||||
key={props.key}
|
|
||||||
onClick={props.onClick}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<Group position="apart">
|
|
||||||
<Group align="center">
|
|
||||||
<Flex>{leftIcon}</Flex>
|
|
||||||
<Box mr="2rem">{children}</Box>
|
|
||||||
</Group>
|
|
||||||
<Box>{rightIcon}</Box>
|
|
||||||
</Group>
|
|
||||||
</StyledContextMenuButton>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const variants: Variants = {
|
|
||||||
closed: {
|
|
||||||
opacity: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
open: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
duration: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContextMenu = forwardRef(
|
|
||||||
({ children, maxWidth, minWidth, xPos, yPos }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
|
||||||
return (
|
|
||||||
<ContextMenuContainer
|
|
||||||
animate="open"
|
|
||||||
initial="closed"
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
minWidth={minWidth}
|
|
||||||
ref={ref}
|
|
||||||
variants={variants}
|
|
||||||
xPos={xPos}
|
|
||||||
yPos={yPos}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextMenuContainer>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import type { DatePickerProps as MantineDatePickerProps } from '@mantine/dates';
|
|
||||||
|
|
||||||
import { DatePicker as MantineDatePicker } from '@mantine/dates';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
interface DatePickerProps extends MantineDatePickerProps {
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledDatePicker = styled(MantineDatePicker)<DatePickerProps>`
|
|
||||||
& .mantine-DatePicker-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-DatePicker-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-DatePicker-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-DatePicker-label {
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-DateRangePicker-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DatePicker = ({ maxWidth, width, ...props }: DatePickerProps) => {
|
|
||||||
return (
|
|
||||||
<StyledDatePicker
|
|
||||||
{...props}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import type { DialogProps as MantineDialogProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Dialog as MantineDialog } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledDialog = styled(MantineDialog)`
|
|
||||||
&.mantine-Dialog-root {
|
|
||||||
background-color: var(--modal-bg);
|
|
||||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Dialog = ({ ...props }: MantineDialogProps) => {
|
|
||||||
return <StyledDialog {...props} />;
|
|
||||||
};
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
import type {
|
|
||||||
MenuDividerProps as MantineMenuDividerProps,
|
|
||||||
MenuDropdownProps as MantineMenuDropdownProps,
|
|
||||||
MenuItemProps as MantineMenuItemProps,
|
|
||||||
MenuLabelProps as MantineMenuLabelProps,
|
|
||||||
MenuProps as MantineMenuProps,
|
|
||||||
} from '@mantine/core';
|
|
||||||
|
|
||||||
import { createPolymorphicComponent, Menu as MantineMenu } from '@mantine/core';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { RiArrowLeftSFill } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type MenuDividerProps = MantineMenuDividerProps;
|
|
||||||
type MenuDropdownProps = MantineMenuDropdownProps;
|
|
||||||
interface MenuItemProps extends MantineMenuItemProps {
|
|
||||||
$danger?: boolean;
|
|
||||||
$isActive?: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
type MenuLabelProps = MantineMenuLabelProps;
|
|
||||||
type MenuProps = MantineMenuProps;
|
|
||||||
|
|
||||||
const StyledMenu = styled(MantineMenu)<MenuProps>``;
|
|
||||||
|
|
||||||
const StyledMenuLabel = styled(MantineMenu.Label)<MenuLabelProps>`
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-family: var(--content-font-family);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledMenuItem = styled(MantineMenu.Item)<MenuItemProps>`
|
|
||||||
position: relative;
|
|
||||||
padding: var(--dropdown-menu-item-padding);
|
|
||||||
font-family: var(--content-font-family);
|
|
||||||
font-size: var(--dropdown-menu-item-font-size);
|
|
||||||
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--dropdown-menu-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Menu-itemLabel {
|
|
||||||
margin-right: 2rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
color: ${(props) => (props.$danger ? 'var(--danger-color)' : 'var(--dropdown-menu-fg)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Menu-itemRightSection {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
background: var(--dropdown-menu-bg);
|
|
||||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 50%));
|
|
||||||
border: var(--dropdown-menu-border);
|
|
||||||
border-radius: var(--dropdown-menu-border-radius);
|
|
||||||
|
|
||||||
/* *:first-child {
|
|
||||||
border-top-left-radius: var(--dropdown-menu-border-radius);
|
|
||||||
border-top-right-radius: var(--dropdown-menu-border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
*:last-child {
|
|
||||||
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
|
||||||
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
|
||||||
} */
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledMenuDivider = styled(MantineMenu.Divider)`
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
|
||||||
return (
|
|
||||||
<StyledMenu
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
transitionProps={{
|
|
||||||
transition: 'fade',
|
|
||||||
}}
|
|
||||||
withinPortal
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
|
|
||||||
return <StyledMenuLabel {...props}>{children}</StyledMenuLabel>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pMenuItem = ({ $danger, $isActive, children, ...props }: MenuItemProps) => {
|
|
||||||
return (
|
|
||||||
<StyledMenuItem
|
|
||||||
$danger={$danger}
|
|
||||||
$isActive={$isActive}
|
|
||||||
rightSection={$isActive && <RiArrowLeftSFill size={15} />}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => {
|
|
||||||
return <StyledMenuDropdown {...props}>{children}</StyledMenuDropdown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuItem = createPolymorphicComponent<'button', MenuItemProps>(pMenuItem);
|
|
||||||
|
|
||||||
const MenuDivider = ({ ...props }: MenuDividerProps) => {
|
|
||||||
return <StyledMenuDivider {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
DropdownMenu.Label = MenuLabel;
|
|
||||||
DropdownMenu.Item = MenuItem;
|
|
||||||
DropdownMenu.Target = MantineMenu.Target;
|
|
||||||
DropdownMenu.Dropdown = MenuDropdown;
|
|
||||||
DropdownMenu.Divider = MenuDivider;
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
.carousel {
|
||||||
|
position: relative;
|
||||||
|
height: 35vh;
|
||||||
|
min-height: 250px;
|
||||||
|
max-height: 300px;
|
||||||
|
padding: var(--theme-spacing-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'image info';
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-columns: 200px minmax(0, 1fr);
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-column {
|
||||||
|
z-index: 15;
|
||||||
|
display: flex;
|
||||||
|
grid-area: image;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-column {
|
||||||
|
z-index: 15;
|
||||||
|
display: flex;
|
||||||
|
grid-area: info;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: 150%;
|
||||||
|
height: 150%;
|
||||||
|
user-select: none;
|
||||||
|
object-fit: var(--theme-image-fit);
|
||||||
|
object-position: 0 30%;
|
||||||
|
filter: blur(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(180deg, rgb(25 26 28 / 30%), var(--theme-colors-background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
@ -1,99 +1,28 @@
|
||||||
import type { Variants } from 'framer-motion';
|
import type { Variants } from 'motion/react';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { Group, Image, Stack } from '@mantine/core';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { Badge } from '/@/renderer/components/badge';
|
import styles from './feature-carousel.module.css';
|
||||||
import { Button } from '/@/renderer/components/button';
|
|
||||||
import { TextTitle } from '/@/renderer/components/text-title';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||||
|
import { PlayButton } from '/@/renderer/features/shared';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
|
import { Badge } from '/@/shared/components/badge/badge';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Image } from '/@/shared/components/image/image';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Album, LibraryItem } from '/@/shared/types/domain-types';
|
import { Album, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
const Carousel = styled(motion.div)`
|
|
||||||
position: relative;
|
|
||||||
height: 35vh;
|
|
||||||
min-height: 250px;
|
|
||||||
padding: 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(180deg, var(--main-bg), rgb(25 26 28 / 60%));
|
|
||||||
border-radius: 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Grid = styled.div`
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'image info';
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
grid-template-columns: 200px minmax(0, 1fr);
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageColumn = styled.div`
|
|
||||||
z-index: 15;
|
|
||||||
display: flex;
|
|
||||||
grid-area: image;
|
|
||||||
align-items: flex-end;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InfoColumn = styled.div`
|
|
||||||
z-index: 15;
|
|
||||||
display: flex;
|
|
||||||
grid-area: info;
|
|
||||||
align-items: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
padding-left: 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BackgroundImage = styled.img`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 150%;
|
|
||||||
height: 150%;
|
|
||||||
user-select: none;
|
|
||||||
object-fit: var(--image-fit);
|
|
||||||
object-position: 0 30%;
|
|
||||||
filter: blur(24px);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BackgroundImageOverlay = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(180deg, rgb(25 26 28 / 30%), var(--main-bg));
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled(Link)`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TitleWrapper = styled.div`
|
|
||||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
animate: {
|
animate: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|
@ -144,7 +73,8 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Link
|
||||||
|
className={styles.wrapper}
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}
|
||||||
>
|
>
|
||||||
<AnimatePresence
|
<AnimatePresence
|
||||||
|
|
@ -153,73 +83,61 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||||
mode="popLayout"
|
mode="popLayout"
|
||||||
>
|
>
|
||||||
{data && (
|
{data && (
|
||||||
<Carousel
|
<motion.div
|
||||||
animate="animate"
|
animate="animate"
|
||||||
|
className={styles.carousel}
|
||||||
custom={direction}
|
custom={direction}
|
||||||
exit="exit"
|
exit="exit"
|
||||||
initial="initial"
|
initial="initial"
|
||||||
key={`image-${itemIndex}`}
|
key={`image-${itemIndex}`}
|
||||||
variants={variants}
|
variants={variants}
|
||||||
>
|
>
|
||||||
<Grid>
|
<div className={styles.grid}>
|
||||||
<ImageColumn>
|
<div className={styles.imageColumn}>
|
||||||
<Image
|
<Image
|
||||||
height={225}
|
height={225}
|
||||||
placeholder="var(--card-default-bg)"
|
src={data[itemIndex]?.imageUrl || ''}
|
||||||
radius="md"
|
|
||||||
src={data[itemIndex]?.imageUrl}
|
|
||||||
sx={{ objectFit: 'cover' }}
|
|
||||||
width={225}
|
width={225}
|
||||||
/>
|
/>
|
||||||
</ImageColumn>
|
</div>
|
||||||
<InfoColumn>
|
<div className={styles.infoColumn}>
|
||||||
<Stack
|
<Stack
|
||||||
spacing="md"
|
gap="md"
|
||||||
sx={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<TitleWrapper>
|
<div className={styles.titleWrapper}>
|
||||||
<TextTitle
|
<TextTitle
|
||||||
lh="3.5rem"
|
fw={900}
|
||||||
|
lineClamp={2}
|
||||||
order={1}
|
order={1}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
sx={{ fontSize: '3.5rem' }}
|
|
||||||
weight={900}
|
|
||||||
>
|
>
|
||||||
{currentItem?.name}
|
{currentItem?.name}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
</TitleWrapper>
|
</div>
|
||||||
<TitleWrapper>
|
<div className={styles.titleWrapper}>
|
||||||
{currentItem?.albumArtists.slice(0, 1).map((artist) => (
|
{currentItem?.albumArtists.slice(0, 1).map((artist) => (
|
||||||
<TextTitle
|
<Text
|
||||||
|
fw={600}
|
||||||
key={`carousel-artist-${artist.id}`}
|
key={`carousel-artist-${artist.id}`}
|
||||||
order={2}
|
|
||||||
weight={600}
|
|
||||||
>
|
>
|
||||||
{artist.name}
|
{artist.name}
|
||||||
</TextTitle>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</TitleWrapper>
|
</div>
|
||||||
<Group>
|
<Group>
|
||||||
{currentItem?.genres?.slice(0, 1).map((genre) => (
|
{currentItem?.genres?.slice(0, 1).map((genre) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={`carousel-genre-${genre.id}`}
|
key={`carousel-genre-${genre.id}`}
|
||||||
size="lg"
|
variant="default"
|
||||||
>
|
>
|
||||||
{genre.name}
|
{genre.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
<Badge variant="default">{currentItem?.releaseYear}</Badge>
|
||||||
{currentItem?.songCount !== null &&
|
|
||||||
currentItem?.songCount !== undefined && (
|
|
||||||
<Badge size="lg">
|
|
||||||
{t('entity.trackWithCount', {
|
|
||||||
count: currentItem?.songCount || 0,
|
|
||||||
})}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
<Group position="apart">
|
<Group justify="space-between">
|
||||||
<Button
|
<PlayButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -233,8 +151,6 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||||
playType,
|
playType,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size="lg"
|
|
||||||
style={{ borderRadius: '5rem' }}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
|
|
@ -245,37 +161,36 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||||
: 'player.addLast',
|
: 'player.addLast',
|
||||||
{ postProcess: 'titleCase' },
|
{ postProcess: 'titleCase' },
|
||||||
)}
|
)}
|
||||||
</Button>
|
</PlayButton>
|
||||||
<Group spacing="sm">
|
<Group gap="sm">
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePrevious}
|
onClick={handlePrevious}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="sm"
|
variant="subtle"
|
||||||
variant="outline"
|
|
||||||
>
|
>
|
||||||
<RiArrowLeftSLine size="2rem" />
|
<Icon icon="arrowLeftS" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="sm"
|
variant="subtle"
|
||||||
variant="outline"
|
|
||||||
>
|
>
|
||||||
<RiArrowRightSLine size="2rem" />
|
<Icon icon="arrowRightS" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</InfoColumn>
|
</div>
|
||||||
</Grid>
|
</div>
|
||||||
<BackgroundImage
|
<Image
|
||||||
|
className={styles.backgroundImage}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={currentItem?.imageUrl || undefined}
|
src={currentItem?.imageUrl || ''}
|
||||||
/>
|
/>
|
||||||
<BackgroundImageOverlay />
|
<div className={styles.backgroundImageOverlay} />
|
||||||
</Carousel>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Wrapper>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Group, Stack } from '@mantine/core';
|
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import {
|
import {
|
||||||
isValidElement,
|
isValidElement,
|
||||||
|
|
@ -11,19 +10,20 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { SwiperOptions, Virtual } from 'swiper';
|
import { SwiperOptions, Virtual } from 'swiper';
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { Swiper as SwiperCore } from 'swiper/types';
|
import { Swiper as SwiperCore } from 'swiper/types';
|
||||||
|
|
||||||
import { Button } from '/@/renderer/components/button';
|
|
||||||
import { PosterCard } from '/@/renderer/components/card/poster-card';
|
import { PosterCard } from '/@/renderer/components/card/poster-card';
|
||||||
import { TextTitle } from '/@/renderer/components/text-title';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
|
|
@ -44,10 +44,6 @@ const getSlidesPerView = (windowWidth: number) => {
|
||||||
return 10;
|
return 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CarouselContainer = styled(Stack)`
|
|
||||||
container-type: inline-size;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface TitleProps {
|
interface TitleProps {
|
||||||
handleNext?: () => void;
|
handleNext?: () => void;
|
||||||
handlePrev?: () => void;
|
handlePrev?: () => void;
|
||||||
|
|
@ -60,36 +56,34 @@ interface TitleProps {
|
||||||
|
|
||||||
const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
|
const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
|
||||||
return (
|
return (
|
||||||
<Group position="apart">
|
<Group justify="space-between">
|
||||||
{isValidElement(label) ? (
|
{isValidElement(label) ? (
|
||||||
label
|
label
|
||||||
) : (
|
) : (
|
||||||
<TextTitle
|
<TextTitle
|
||||||
order={2}
|
order={3}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group spacing="sm">
|
<Group gap="sm">
|
||||||
<Button
|
<Button
|
||||||
compact
|
|
||||||
disabled={!pagination.hasPreviousPage}
|
disabled={!pagination.hasPreviousPage}
|
||||||
onClick={handlePrev}
|
onClick={handlePrev}
|
||||||
size="lg"
|
size="compact-md"
|
||||||
variant="default"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
<RiArrowLeftSLine />
|
<Icon icon="arrowLeftS" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
|
||||||
disabled={!pagination.hasNextPage}
|
disabled={!pagination.hasNextPage}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
size="lg"
|
size="compact-md"
|
||||||
variant="default"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
<RiArrowRightSLine />
|
<Icon icon="arrowRightS" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
@ -286,10 +280,10 @@ export const SwiperGridCarousel = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CarouselContainer
|
<Stack
|
||||||
className="grid-carousel"
|
className="grid-carousel"
|
||||||
ref={containerRef}
|
gap="md"
|
||||||
spacing="md"
|
ref={containerRef as any}
|
||||||
>
|
>
|
||||||
{title ? (
|
{title ? (
|
||||||
<Title
|
<Title
|
||||||
|
|
@ -326,7 +320,7 @@ export const SwiperGridCarousel = ({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
</CarouselContainer>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { HoverCardProps, HoverCard as MantineHoverCard } from '@mantine/core';
|
|
||||||
|
|
||||||
export const HoverCard = ({ children, ...props }: HoverCardProps) => {
|
|
||||||
return (
|
|
||||||
<MantineHoverCard
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
background: 'var(--dropdown-menu-bg)',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: 'var(--dropdown-menu-border-radius)',
|
|
||||||
boxShadow: '2px 2px 10px 2px rgba(0, 0, 0, 40%)',
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MantineHoverCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
HoverCard.Target = MantineHoverCard.Target;
|
|
||||||
HoverCard.Dropdown = MantineHoverCard.Dropdown;
|
|
||||||
|
|
@ -1,36 +1,2 @@
|
||||||
export * from './accordion';
|
|
||||||
export * from './audio-player';
|
export * from './audio-player';
|
||||||
export * from './badge';
|
|
||||||
export * from './button';
|
|
||||||
export * from './card';
|
|
||||||
export * from './checkbox';
|
|
||||||
export * from './context-menu';
|
|
||||||
export * from './date-picker';
|
|
||||||
export * from './dialog';
|
|
||||||
export * from './dropdown-menu';
|
|
||||||
export * from './feature-carousel';
|
|
||||||
export * from './hover-card';
|
|
||||||
export * from './input';
|
|
||||||
export * from './modal';
|
|
||||||
export * from './motion';
|
export * from './motion';
|
||||||
export * from './option';
|
|
||||||
export * from './page-header';
|
|
||||||
export * from './pagination';
|
|
||||||
export * from './paper';
|
|
||||||
export * from './popover';
|
|
||||||
export * from './query-builder';
|
|
||||||
export * from './rating';
|
|
||||||
export * from './scroll-area';
|
|
||||||
export * from './search-input';
|
|
||||||
export * from './segmented-control';
|
|
||||||
export * from './select';
|
|
||||||
export * from './skeleton';
|
|
||||||
export * from './slider';
|
|
||||||
export * from './spinner';
|
|
||||||
export * from './spoiler';
|
|
||||||
export * from './switch';
|
|
||||||
export * from './tabs';
|
|
||||||
export * from './text';
|
|
||||||
export * from './text-title';
|
|
||||||
export * from './toast';
|
|
||||||
export * from './tooltip';
|
|
||||||
|
|
|
||||||
|
|
@ -1,369 +0,0 @@
|
||||||
import type {
|
|
||||||
FileInputProps as MantineFileInputProps,
|
|
||||||
JsonInputProps as MantineJsonInputProps,
|
|
||||||
NumberInputProps as MantineNumberInputProps,
|
|
||||||
PasswordInputProps as MantinePasswordInputProps,
|
|
||||||
TextareaProps as MantineTextareaProps,
|
|
||||||
TextInputProps as MantineTextInputProps,
|
|
||||||
} from '@mantine/core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
FileInput as MantineFileInput,
|
|
||||||
JsonInput as MantineJsonInput,
|
|
||||||
NumberInput as MantineNumberInput,
|
|
||||||
PasswordInput as MantinePasswordInput,
|
|
||||||
Textarea as MantineTextarea,
|
|
||||||
TextInput as MantineTextInput,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import React, { forwardRef } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
interface FileInputProps extends MantineFileInputProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JsonInputProps extends MantineJsonInputProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NumberInputProps extends MantineNumberInputProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PasswordInputProps extends MantinePasswordInputProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextareaProps extends MantineTextareaProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextInputProps extends MantineTextInputProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledTextInput = styled(MantineTextInput)<TextInputProps>`
|
|
||||||
& .mantine-TextInput-wrapper {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-TextInput-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Input-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-TextInput-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-TextInput-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-TextInput-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledNumberInput = styled(MantineNumberInput)<NumberInputProps>`
|
|
||||||
& .mantine-NumberInput-wrapper {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-controlUp {
|
|
||||||
svg {
|
|
||||||
color: var(--btn-default-fg);
|
|
||||||
fill: var(--btn-default-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-controlDown {
|
|
||||||
svg {
|
|
||||||
color: var(--btn-default-fg);
|
|
||||||
fill: var(--btn-default-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Input-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledPasswordInput = styled(MantinePasswordInput)<PasswordInputProps>`
|
|
||||||
& .mantine-PasswordInput-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-PasswordInput-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-PasswordInput-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-PasswordInput-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-PasswordInput-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledFileInput = styled(MantineFileInput)<FileInputProps>`
|
|
||||||
& .mantine-FileInput-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-FileInput-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-FileInput-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-FileInput-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-FileInput-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledJsonInput = styled(MantineJsonInput)<JsonInputProps>`
|
|
||||||
& .mantine-JsonInput-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-JsonInput-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-JsonInput-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-JsonInput-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-JsonInput-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTextarea = styled(MantineTextarea)<TextareaProps>`
|
|
||||||
& .mantine-Textarea-input {
|
|
||||||
color: var(--input-fg);
|
|
||||||
background: var(--input-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Textarea-icon {
|
|
||||||
color: var(--input-placeholder-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Textarea-required {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Textarea-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Textarea-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|
||||||
({ children, maxWidth, width, ...props }: TextInputProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledTextInput
|
|
||||||
ref={ref}
|
|
||||||
spellCheck={false}
|
|
||||||
{...props}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledTextInput>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
||||||
({ children, maxWidth, width, ...props }: NumberInputProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledNumberInput
|
|
||||||
hideControls
|
|
||||||
ref={ref}
|
|
||||||
spellCheck={false}
|
|
||||||
{...props}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledNumberInput>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
||||||
({ children, maxWidth, width, ...props }: PasswordInputProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledPasswordInput
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledPasswordInput>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
|
|
||||||
({ children, maxWidth, width, ...props }: FileInputProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledFileInput
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
styles={{
|
|
||||||
placeholder: {
|
|
||||||
color: 'var(--input-placeholder-fg)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledFileInput>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
|
|
||||||
({ children, maxWidth, width, ...props }: JsonInputProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledJsonInput
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledJsonInput>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
||||||
({ children, maxWidth, width, ...props }: TextareaProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledTextarea
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledTextarea>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { motion } from 'motion/react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export const MotionFlex = motion(Flex);
|
import { Flex, FlexProps } from '/@/shared/components/flex/flex';
|
||||||
|
import { Group, GroupProps } from '/@/shared/components/group/group';
|
||||||
|
import { Stack, StackProps } from '/@/shared/components/stack/stack';
|
||||||
|
|
||||||
export const MotionGroup = motion(Group);
|
export const MotionFlex = motion.create<FlexProps>(Flex, { forwardMotionProps: true });
|
||||||
|
|
||||||
export const MotionStack = motion(Stack);
|
export const MotionGroup = motion.create<GroupProps>(Group, { forwardMotionProps: true });
|
||||||
|
|
||||||
|
export const MotionStack = motion.create<StackProps>(Stack, { forwardMotionProps: true });
|
||||||
|
|
||||||
export const MotionDiv = motion.div;
|
export const MotionDiv = motion.div;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
.scroll-area {
|
||||||
|
height: calc(100vh - 90px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
width: calc(100% - 130px);
|
||||||
|
height: 65px;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,73 +1,14 @@
|
||||||
import type { ScrollAreaProps as MantineScrollAreaProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { ScrollArea as MantineScrollArea } from '@mantine/core';
|
|
||||||
import { useMergedRef } from '@mantine/hooks';
|
import { useMergedRef } from '@mantine/hooks';
|
||||||
import { useInView } from 'framer-motion';
|
import { useInView } from 'motion/react';
|
||||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
import { CSSProperties, forwardRef, ReactNode, Ref, useEffect, useRef, useState } from 'react';
|
import { CSSProperties, forwardRef, ReactNode, Ref, useEffect, useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
|
import styles from './native-scroll-area.module.css';
|
||||||
|
|
||||||
|
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header/page-header';
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
const DragContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: -1;
|
|
||||||
width: calc(100% - 130px);
|
|
||||||
height: 65px;
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
|
|
||||||
button {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ScrollAreaProps extends MantineScrollAreaProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledScrollArea = styled(MantineScrollArea)`
|
|
||||||
& .mantine-ScrollArea-thumb {
|
|
||||||
background: var(--scrollbar-thumb-bg);
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-ScrollArea-scrollbar {
|
|
||||||
padding: 0;
|
|
||||||
background: var(--scrollbar-track-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-ScrollArea-viewport > div {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledNativeScrollArea = styled.div<{
|
|
||||||
$scrollBarOffset?: string;
|
|
||||||
$windowBarStyle?: Platform;
|
|
||||||
}>`
|
|
||||||
height: calc(100vh - 90px);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
|
|
||||||
return (
|
|
||||||
<StyledScrollArea
|
|
||||||
ref={ref}
|
|
||||||
scrollbarSize={12}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledScrollArea>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
interface NativeScrollAreaProps {
|
interface NativeScrollAreaProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
debugScrollPosition?: boolean;
|
debugScrollPosition?: boolean;
|
||||||
|
|
@ -80,14 +21,7 @@ interface NativeScrollAreaProps {
|
||||||
|
|
||||||
export const NativeScrollArea = forwardRef(
|
export const NativeScrollArea = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{ children, noHeader, pageHeaderProps, scrollHideDelay, ...props }: NativeScrollAreaProps,
|
||||||
children,
|
|
||||||
noHeader,
|
|
||||||
pageHeaderProps,
|
|
||||||
scrollBarOffset,
|
|
||||||
scrollHideDelay,
|
|
||||||
...props
|
|
||||||
}: NativeScrollAreaProps,
|
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
@ -130,7 +64,7 @@ export const NativeScrollArea = forwardRef(
|
||||||
autoHide: 'leave',
|
autoHide: 'leave',
|
||||||
autoHideDelay: scrollHideDelay || 500,
|
autoHideDelay: scrollHideDelay || 500,
|
||||||
pointers: ['mouse', 'pen', 'touch'],
|
pointers: ['mouse', 'pen', 'touch'],
|
||||||
theme: 'feishin',
|
theme: 'feishin-os-scrollbar',
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -148,7 +82,7 @@ export const NativeScrollArea = forwardRef(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{windowBarStyle === Platform.WEB && <DragContainer />}
|
{windowBarStyle === Platform.WEB && <div className={styles.dragContainer} />}
|
||||||
{shouldShowHeader && (
|
{shouldShowHeader && (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
animated
|
animated
|
||||||
|
|
@ -157,14 +91,13 @@ export const NativeScrollArea = forwardRef(
|
||||||
{...pageHeaderProps}
|
{...pageHeaderProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StyledNativeScrollArea
|
<div
|
||||||
$scrollBarOffset={scrollBarOffset}
|
className={styles.scrollArea}
|
||||||
$windowBarStyle={windowBarStyle}
|
|
||||||
ref={mergedRef}
|
ref={mergedRef}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledNativeScrollArea>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { Flex, Group } from '@mantine/core';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export const Option = ({ children }: any) => {
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
grow
|
|
||||||
p="0.5rem"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LabelProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Label = ({ children }: LabelProps) => {
|
|
||||||
return <Flex align="flex-start">{children}</Flex>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ControlProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Control = ({ children }: ControlProps) => {
|
|
||||||
return <Flex justify="flex-end">{children}</Flex>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Option.Label = Label;
|
|
||||||
Option.Control = Control;
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
import { Flex, FlexProps } from '@mantine/core';
|
|
||||||
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
|
||||||
import { ReactNode, useRef } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
|
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
|
||||||
import { Platform } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const Container = styled(motion(Flex))<{
|
|
||||||
$height?: string;
|
|
||||||
$position?: string;
|
|
||||||
}>`
|
|
||||||
position: ${(props) => props.$position || 'relative'};
|
|
||||||
z-index: 190;
|
|
||||||
width: 100%;
|
|
||||||
height: ${(props) => props.$height || '65px'};
|
|
||||||
background: var(--titlebar-bg);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Header = styled(motion.div)<{
|
|
||||||
$isDraggable?: boolean;
|
|
||||||
$isHidden?: boolean;
|
|
||||||
$padRight?: boolean;
|
|
||||||
}>`
|
|
||||||
position: relative;
|
|
||||||
z-index: 15;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin-right: ${(props) => (props.$padRight ? '140px' : '1rem')};
|
|
||||||
pointer-events: ${(props) => (props.$isHidden ? 'none' : 'auto')};
|
|
||||||
user-select: ${(props) => (props.$isHidden ? 'none' : 'auto')};
|
|
||||||
-webkit-app-region: ${(props) => props.$isDraggable && 'drag'};
|
|
||||||
|
|
||||||
button {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BackgroundImage = styled.div<{ $background: string }>`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: ${(props) => props.$background || 'var(--titlebar-bg)'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BackgroundImageOverlay = styled.div<{ theme: 'dark' | 'light' }>`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: ${(props) =>
|
|
||||||
props.theme === 'light'
|
|
||||||
? 'linear-gradient(rgba(255, 255, 255, 25%), rgba(255, 255, 255, 25%))'
|
|
||||||
: 'linear-gradient(rgba(0, 0, 0, 50%), rgba(0, 0, 0, 50%))'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export interface PageHeaderProps
|
|
||||||
extends Omit<FlexProps, 'onAnimationStart' | 'onDrag' | 'onDragEnd' | 'onDragStart'> {
|
|
||||||
animated?: boolean;
|
|
||||||
backgroundColor?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
height?: string;
|
|
||||||
isHidden?: boolean;
|
|
||||||
position?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TitleWrapper = styled(motion.div)`
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const variants: Variants = {
|
|
||||||
animate: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
duration: 0.3,
|
|
||||||
ease: 'easeIn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exit: { opacity: 0 },
|
|
||||||
initial: { opacity: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageHeader = ({
|
|
||||||
animated,
|
|
||||||
backgroundColor,
|
|
||||||
children,
|
|
||||||
height,
|
|
||||||
isHidden,
|
|
||||||
position,
|
|
||||||
...props
|
|
||||||
}: PageHeaderProps) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
const padRight = useShouldPadTitlebar();
|
|
||||||
const { windowBarStyle } = useWindowSettings();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Container
|
|
||||||
$height={height}
|
|
||||||
$position={position}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Header
|
|
||||||
$isDraggable={windowBarStyle === Platform.WEB}
|
|
||||||
$isHidden={isHidden}
|
|
||||||
$padRight={padRight}
|
|
||||||
>
|
|
||||||
<AnimatePresence initial={animated ?? false}>
|
|
||||||
<TitleWrapper
|
|
||||||
animate="animate"
|
|
||||||
exit="exit"
|
|
||||||
initial="initial"
|
|
||||||
variants={variants}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</TitleWrapper>
|
|
||||||
</AnimatePresence>
|
|
||||||
</Header>
|
|
||||||
{backgroundColor && (
|
|
||||||
<>
|
|
||||||
<BackgroundImage $background={backgroundColor || 'var(--titlebar-bg)'} />
|
|
||||||
<BackgroundImageOverlay theme={theme as 'dark' | 'light'} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
75
src/renderer/components/page-header/page-header.module.css
Normal file
75
src/renderer/components/page-header/page-header.module.css
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 190;
|
||||||
|
width: 100%;
|
||||||
|
height: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 15;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 1rem;
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: auto;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.pad-right {
|
||||||
|
margin-right: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.is-draggable {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--theme-colors-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image-overlay.light {
|
||||||
|
background: linear-gradient(rgb(255 255 255 / 25%), rgb(255 255 255 / 25%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image-overlay.dark {
|
||||||
|
background: linear-gradient(rgb(0 0 0 / 50%), rgb(0 0 0 / 50%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
95
src/renderer/components/page-header/page-header.tsx
Normal file
95
src/renderer/components/page-header/page-header.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { AnimatePresence, motion, Variants } from 'motion/react';
|
||||||
|
import { CSSProperties, ReactNode, useRef } from 'react';
|
||||||
|
|
||||||
|
import styles from './page-header.module.css';
|
||||||
|
|
||||||
|
import { useShouldPadTitlebar } from '/@/renderer/hooks';
|
||||||
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
|
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||||
|
import { Flex, FlexProps } from '/@/shared/components/flex/flex';
|
||||||
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export interface PageHeaderProps
|
||||||
|
extends Omit<FlexProps, 'onAnimationStart' | 'onDrag' | 'onDragEnd' | 'onDragStart'> {
|
||||||
|
animated?: boolean;
|
||||||
|
backgroundColor?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
height?: string;
|
||||||
|
isHidden?: boolean;
|
||||||
|
position?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeIn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: { opacity: 0 },
|
||||||
|
initial: { opacity: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageHeader = ({
|
||||||
|
animated,
|
||||||
|
backgroundColor = 'var(--theme-colors-background)',
|
||||||
|
children,
|
||||||
|
height,
|
||||||
|
isHidden,
|
||||||
|
position,
|
||||||
|
...props
|
||||||
|
}: PageHeaderProps) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const padRight = useShouldPadTitlebar();
|
||||||
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
const { mode } = useAppTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
className={styles.container}
|
||||||
|
ref={ref}
|
||||||
|
style={{ height, position: position as CSSProperties['position'] }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.header, {
|
||||||
|
[styles.hidden]: isHidden,
|
||||||
|
[styles.isDraggable]: windowBarStyle === Platform.WEB,
|
||||||
|
[styles.padRight]: padRight,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AnimatePresence initial={animated ?? false}>
|
||||||
|
<motion.div
|
||||||
|
animate="animate"
|
||||||
|
className={styles.titleWrapper}
|
||||||
|
exit="exit"
|
||||||
|
initial="initial"
|
||||||
|
variants={variants}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
{backgroundColor && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={styles.backgroundImage}
|
||||||
|
style={{
|
||||||
|
background: backgroundColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.backgroundImageOverlay, {
|
||||||
|
[styles.dark]: mode === 'dark',
|
||||||
|
[styles.light]: mode === 'light',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import {
|
|
||||||
Pagination as MantinePagination,
|
|
||||||
PaginationProps as MantinePaginationProps,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledPagination = styled(MantinePagination)<PaginationProps>`
|
|
||||||
& .mantine-Pagination-item {
|
|
||||||
color: var(--btn-default-fg);
|
|
||||||
background-color: var(--btn-default-bg);
|
|
||||||
border: none;
|
|
||||||
transition:
|
|
||||||
background 0.2s ease-in-out,
|
|
||||||
color 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&[data-active] {
|
|
||||||
color: var(--btn-primary-fg);
|
|
||||||
background-color: var(--btn-primary-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-dots] {
|
|
||||||
display: ${({ $hideDividers }) => ($hideDividers ? 'none' : 'block')};
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--btn-default-fg-hover);
|
|
||||||
background-color: var(--btn-default-bg-hover);
|
|
||||||
|
|
||||||
&[data-active] {
|
|
||||||
color: var(--btn-primary-fg-hover);
|
|
||||||
background-color: var(--btn-primary-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-dots] {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface PaginationProps extends MantinePaginationProps {
|
|
||||||
$hideDividers?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Pagination = ({ $hideDividers, ...props }: PaginationProps) => {
|
|
||||||
return (
|
|
||||||
<StyledPagination
|
|
||||||
$hideDividers={$hideDividers}
|
|
||||||
radius="xl"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import type { PaperProps as MantinePaperProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Paper as MantinePaper } from '@mantine/core';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export interface PaperProps extends MantinePaperProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledPaper = styled(MantinePaper)<PaperProps>`
|
|
||||||
background: var(--paper-bg);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Paper = ({ children, ...props }: PaperProps) => {
|
|
||||||
return <StyledPaper {...props}>{children}</StyledPaper>;
|
|
||||||
};
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import type {
|
|
||||||
PopoverDropdownProps as MantinePopoverDropdownProps,
|
|
||||||
PopoverProps as MantinePopoverProps,
|
|
||||||
} from '@mantine/core';
|
|
||||||
|
|
||||||
import { Popover as MantinePopover } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type PopoverDropdownProps = MantinePopoverDropdownProps;
|
|
||||||
type PopoverProps = MantinePopoverProps;
|
|
||||||
|
|
||||||
const StyledPopover = styled(MantinePopover)``;
|
|
||||||
|
|
||||||
const StyledDropdown = styled(MantinePopover.Dropdown)<PopoverDropdownProps>`
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-family: var(--content-font-family);
|
|
||||||
font-size: 0.9em;
|
|
||||||
background-color: var(--dropdown-menu-bg);
|
|
||||||
border: var(--dropdown-menu-border);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Popover = ({ children, ...props }: PopoverProps) => {
|
|
||||||
return (
|
|
||||||
<StyledPopover
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
transitionProps={{ transition: 'fade' }}
|
|
||||||
withinPortal
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledPopover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Popover.Target = MantinePopover.Target;
|
|
||||||
Popover.Dropdown = StyledDropdown;
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { RiAddFill, RiAddLine, RiDeleteBinFill, RiMore2Line, RiRestartLine } from 'react-icons/ri';
|
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { Button } from '/@/renderer/components/button';
|
|
||||||
import { DropdownMenu } from '/@/renderer/components/dropdown-menu';
|
|
||||||
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
|
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
|
||||||
import { Select } from '/@/renderer/components/select';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { QueryBuilderGroup, QueryBuilderRule } from '/@/shared/types/types';
|
import { QueryBuilderGroup, QueryBuilderRule } from '/@/shared/types/types';
|
||||||
|
|
||||||
const FILTER_GROUP_OPTIONS_DATA = [
|
const FILTER_GROUP_OPTIONS_DATA = [
|
||||||
|
|
@ -99,10 +100,10 @@ export const QueryBuilder = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
|
gap="sm"
|
||||||
ml={`${level * 10}px`}
|
ml={`${level * 10}px`}
|
||||||
spacing="sm"
|
|
||||||
>
|
>
|
||||||
<Group spacing="sm">
|
<Group gap="sm">
|
||||||
<Select
|
<Select
|
||||||
data={FILTER_GROUP_OPTIONS_DATA}
|
data={FILTER_GROUP_OPTIONS_DATA}
|
||||||
maxWidth={175}
|
maxWidth={175}
|
||||||
|
|
@ -111,28 +112,26 @@ export const QueryBuilder = ({
|
||||||
value={data.type}
|
value={data.type}
|
||||||
width="20%"
|
width="20%"
|
||||||
/>
|
/>
|
||||||
<Button
|
<ActionIcon
|
||||||
|
icon="add"
|
||||||
onClick={handleAddRule}
|
onClick={handleAddRule}
|
||||||
px={5}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
tooltip={{ label: 'Add rule' }}
|
variant="subtle"
|
||||||
variant="default"
|
/>
|
||||||
>
|
|
||||||
<RiAddLine size={20} />
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom-start">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<ActionIcon
|
||||||
p={0}
|
icon="ellipsisVertical"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
/>
|
||||||
<RiMore2Line size={20} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiAddFill />}
|
leftSection={<Icon icon="add" />}
|
||||||
onClick={handleAddRuleGroup}
|
onClick={handleAddRuleGroup}
|
||||||
>
|
>
|
||||||
Add rule group
|
Add rule group
|
||||||
|
|
@ -140,7 +139,7 @@ export const QueryBuilder = ({
|
||||||
|
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiDeleteBinFill />}
|
leftSection={<Icon icon="delete" />}
|
||||||
onClick={handleDeleteRuleGroup}
|
onClick={handleDeleteRuleGroup}
|
||||||
>
|
>
|
||||||
Remove rule group
|
Remove rule group
|
||||||
|
|
@ -150,15 +149,25 @@ export const QueryBuilder = ({
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
$danger
|
isDanger
|
||||||
icon={<RiRestartLine color="var(--danger-color)" />}
|
leftSection={
|
||||||
|
<Icon
|
||||||
|
color="error"
|
||||||
|
icon="refresh"
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={onResetFilters}
|
onClick={onResetFilters}
|
||||||
>
|
>
|
||||||
Reset to default
|
Reset to default
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
$danger
|
isDanger
|
||||||
icon={<RiDeleteBinFill color="var(--danger-color)" />}
|
leftSection={
|
||||||
|
<Icon
|
||||||
|
color="error"
|
||||||
|
icon="delete"
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={onClearFilters}
|
onClick={onClearFilters}
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Group } from '@mantine/core';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RiSubtractLine } from 'react-icons/ri';
|
|
||||||
|
|
||||||
import { Button } from '/@/renderer/components/button';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { NumberInput, TextInput } from '/@/renderer/components/input';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Select } from '/@/renderer/components/select';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
import { QueryBuilderRule } from '/@/shared/types/types';
|
import { QueryBuilderRule } from '/@/shared/types/types';
|
||||||
|
|
||||||
type DeleteArgs = {
|
type DeleteArgs = {
|
||||||
|
|
@ -33,7 +33,7 @@ interface QueryOptionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
||||||
const [numberRange, setNumberRange] = useState([0, 0]);
|
const [numberRange, setNumberRange] = useState<number[]>([0, 0]);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
|
@ -63,7 +63,7 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
||||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
|
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
|
||||||
maxWidth={81}
|
maxWidth={81}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newRange = [e || 0, numberRange[1]];
|
const newRange = [Number(e) || 0, numberRange[1]];
|
||||||
setNumberRange(newRange);
|
setNumberRange(newRange);
|
||||||
onChange(newRange);
|
onChange(newRange);
|
||||||
}}
|
}}
|
||||||
|
|
@ -74,7 +74,7 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
||||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
|
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
|
||||||
maxWidth={81}
|
maxWidth={81}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newRange = [numberRange[0], e || 0];
|
const newRange = [numberRange[0], Number(e) || 0];
|
||||||
setNumberRange(newRange);
|
setNumberRange(newRange);
|
||||||
onChange(newRange);
|
onChange(newRange);
|
||||||
}}
|
}}
|
||||||
|
|
@ -189,8 +189,8 @@ export const QueryBuilderOption = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
|
gap="sm"
|
||||||
ml={ml}
|
ml={ml}
|
||||||
spacing="sm"
|
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
data={filters}
|
data={filters}
|
||||||
|
|
@ -231,16 +231,14 @@ export const QueryBuilderOption = ({
|
||||||
width="25%"
|
width="25%"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<ActionIcon
|
||||||
disabled={noRemove}
|
disabled={noRemove}
|
||||||
|
icon="remove"
|
||||||
onClick={handleDeleteRule}
|
onClick={handleDeleteRule}
|
||||||
px={5}
|
px={5}
|
||||||
size="sm"
|
size="sm"
|
||||||
tooltip={{ label: 'Remove rule' }}
|
variant="subtle"
|
||||||
variant="default"
|
/>
|
||||||
>
|
|
||||||
<RiSubtractLine size={20} />
|
|
||||||
</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { ActionIcon, TextInputProps } from '@mantine/core';
|
|
||||||
import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks';
|
|
||||||
import { ChangeEvent, KeyboardEvent } from 'react';
|
|
||||||
import { RiCloseFill, RiSearchLine } from 'react-icons/ri';
|
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
|
|
||||||
import { TextInput } from '/@/renderer/components/input';
|
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
|
||||||
|
|
||||||
interface SearchInputProps extends TextInputProps {
|
|
||||||
initialWidth?: number;
|
|
||||||
openedWidth?: number;
|
|
||||||
value?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchInput = ({
|
|
||||||
initialWidth,
|
|
||||||
onChange,
|
|
||||||
openedWidth,
|
|
||||||
...props
|
|
||||||
}: SearchInputProps) => {
|
|
||||||
const { focused, ref } = useFocusWithin();
|
|
||||||
const mergedRef = useMergedRef<HTMLInputElement>(ref);
|
|
||||||
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
|
|
||||||
|
|
||||||
const isOpened = focused || ref.current?.value;
|
|
||||||
const showIcon = !isOpened || (openedWidth || 100) > 100;
|
|
||||||
|
|
||||||
useHotkeys([[binding.hotkey, () => ref.current.select()]]);
|
|
||||||
|
|
||||||
const handleEscape = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.code === 'Escape') {
|
|
||||||
onChange?.({ target: { value: '' } } as ChangeEvent<HTMLInputElement>);
|
|
||||||
ref.current.value = '';
|
|
||||||
ref.current.blur();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
ref={mergedRef}
|
|
||||||
{...props}
|
|
||||||
icon={showIcon && <RiSearchLine />}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={handleEscape}
|
|
||||||
rightSection={
|
|
||||||
isOpened ? (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
ref.current.value = '';
|
|
||||||
ref.current.focus();
|
|
||||||
onChange?.({ target: { value: '' } } as ChangeEvent<HTMLInputElement>);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiCloseFill />
|
|
||||||
</ActionIcon>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
size="md"
|
|
||||||
styles={{
|
|
||||||
icon: { svg: { fill: 'var(--titlebar-fg)' } },
|
|
||||||
input: {
|
|
||||||
backgroundColor: isOpened ? 'inherit' : 'transparent !important',
|
|
||||||
border: 'none !important',
|
|
||||||
cursor: isOpened ? 'text' : 'pointer',
|
|
||||||
padding: isOpened ? '10px' : 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
width={isOpened ? openedWidth || 150 : initialWidth || 35}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import type { SegmentedControlProps as MantineSegmentedControlProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { SegmentedControl as MantineSegmentedControl } from '@mantine/core';
|
|
||||||
import { forwardRef } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type SegmentedControlProps = MantineSegmentedControlProps;
|
|
||||||
|
|
||||||
const StyledSegmentedControl = styled(MantineSegmentedControl)<MantineSegmentedControlProps>`
|
|
||||||
& .mantine-SegmentedControl-label {
|
|
||||||
font-family: var(--content-font-family);
|
|
||||||
color: var(--input-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: var(--input-bg);
|
|
||||||
|
|
||||||
& .mantine-SegmentedControl-disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-SegmentedControl-active {
|
|
||||||
color: var(--input-active-fg);
|
|
||||||
background-color: var(--input-active-bg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
||||||
({ ...props }: SegmentedControlProps, ref) => {
|
|
||||||
return (
|
|
||||||
<StyledSegmentedControl
|
|
||||||
ref={ref}
|
|
||||||
styles={{}}
|
|
||||||
transitionDuration={250}
|
|
||||||
transitionTimingFunction="linear"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelect, MultiSelectProps, Select, SelectProps } from '/@/renderer/components/select';
|
import { MultiSelect, MultiSelectProps } from '/@/shared/components/multi-select/multi-select';
|
||||||
|
import { Select, SelectProps } from '/@/shared/components/select/select';
|
||||||
|
|
||||||
export const SelectWithInvalidData = ({ data, defaultValue, ...props }: SelectProps) => {
|
export const SelectWithInvalidData = ({ data, defaultValue, ...props }: SelectProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -9,12 +10,14 @@ export const SelectWithInvalidData = ({ data, defaultValue, ...props }: SelectPr
|
||||||
const [fullData, hasError] = useMemo(() => {
|
const [fullData, hasError] = useMemo(() => {
|
||||||
if (typeof defaultValue === 'string') {
|
if (typeof defaultValue === 'string') {
|
||||||
const missingField =
|
const missingField =
|
||||||
data.find((item) =>
|
data?.find((item) =>
|
||||||
typeof item === 'string' ? item === defaultValue : item.value === defaultValue,
|
typeof item === 'string'
|
||||||
|
? item === defaultValue
|
||||||
|
: (item as any).value === defaultValue,
|
||||||
) === undefined;
|
) === undefined;
|
||||||
|
|
||||||
if (missingField) {
|
if (missingField) {
|
||||||
return [data.concat(defaultValue), true];
|
return [data?.concat(defaultValue), true];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,11 +43,11 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
|
||||||
const [fullData, missing] = useMemo(() => {
|
const [fullData, missing] = useMemo(() => {
|
||||||
if (defaultValue?.length) {
|
if (defaultValue?.length) {
|
||||||
const validValues = new Set<string>();
|
const validValues = new Set<string>();
|
||||||
for (const item of data) {
|
for (const item of data || []) {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
validValues.add(item);
|
validValues.add(item);
|
||||||
} else {
|
} else {
|
||||||
validValues.add(item.value);
|
validValues.add((item as any).value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +60,7 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
return [data.concat(missingFields), missingFields];
|
return [data?.concat(missingFields), missingFields];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
import type {
|
|
||||||
MultiSelectProps as MantineMultiSelectProps,
|
|
||||||
SelectProps as MantineSelectProps,
|
|
||||||
} from '@mantine/core';
|
|
||||||
|
|
||||||
import { MultiSelect as MantineMultiSelect, Select as MantineSelect } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export interface MultiSelectProps extends MantineMultiSelectProps {
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectProps extends MantineSelectProps {
|
|
||||||
maxWidth?: number | string;
|
|
||||||
width?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledSelect = styled(MantineSelect)`
|
|
||||||
& [data-selected='true'] {
|
|
||||||
background: var(--input-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
background: var(--input-bg);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Select-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-family: var(--label-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Select-itemsWrapper {
|
|
||||||
& .mantine-Select-item {
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Select = ({ maxWidth, width, ...props }: SelectProps) => {
|
|
||||||
return (
|
|
||||||
<StyledSelect
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
background: 'var(--dropdown-menu-bg)',
|
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 20%))',
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
background: 'var(--input-bg)',
|
|
||||||
color: 'var(--input-fg)',
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
'&:hover': {
|
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
|
||||||
},
|
|
||||||
'&[data-hovered]': {
|
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
|
||||||
},
|
|
||||||
'&[data-selected="true"]': {
|
|
||||||
'&:hover': {
|
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
|
||||||
},
|
|
||||||
background: 'none',
|
|
||||||
color: 'var(--primary-color)',
|
|
||||||
},
|
|
||||||
color: 'var(--dropdown-menu-fg)',
|
|
||||||
padding: '.3rem',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
transitionProps={{ duration: 100, transition: 'fade' }}
|
|
||||||
withinPortal
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledMultiSelect = styled(MantineMultiSelect)`
|
|
||||||
& [data-selected='true'] {
|
|
||||||
background: var(--input-select-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
|
||||||
background: var(--input-bg);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-MultiSelect-itemsWrapper {
|
|
||||||
& .mantine-Select-item {
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const MultiSelect = ({ maxWidth, width, ...props }: MultiSelectProps) => {
|
|
||||||
return (
|
|
||||||
<StyledMultiSelect
|
|
||||||
styles={{
|
|
||||||
dropdown: {
|
|
||||||
background: 'var(--dropdown-menu-bg)',
|
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 20%))',
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
background: 'var(--input-bg)',
|
|
||||||
color: 'var(--input-fg)',
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
'&:hover': {
|
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
|
||||||
},
|
|
||||||
'&[data-hovered]': {
|
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
|
||||||
},
|
|
||||||
'&[data-selected="true"]': {
|
|
||||||
'&:hover': {
|
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
|
||||||
},
|
|
||||||
background: 'none',
|
|
||||||
color: 'var(--primary-color)',
|
|
||||||
},
|
|
||||||
color: 'var(--dropdown-menu-fg)',
|
|
||||||
padding: '.5rem .1rem',
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
margin: '.2rem',
|
|
||||||
paddingBottom: '1rem',
|
|
||||||
paddingLeft: '1rem',
|
|
||||||
paddingTop: '1rem',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
sx={{ maxWidth, width }}
|
|
||||||
transitionProps={{ duration: 100, transition: 'fade' }}
|
|
||||||
withinPortal
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
|
||||||
|
|
||||||
export const Separator = () => {
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
$noSelect
|
|
||||||
$secondary
|
|
||||||
size="md"
|
|
||||||
style={{ display: 'inline-block', padding: '0px 3px' }}
|
|
||||||
>
|
|
||||||
{SEPARATOR_STRING}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import type { SkeletonProps as MantineSkeletonProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Skeleton as MantineSkeleton } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledSkeleton = styled(MantineSkeleton)`
|
|
||||||
&::after {
|
|
||||||
background: var(--placeholder-bg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Skeleton = ({ ...props }: MantineSkeletonProps) => {
|
|
||||||
return (
|
|
||||||
<StyledSkeleton
|
|
||||||
animate={false}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import type { SliderProps as MantineSliderProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Slider as MantineSlider } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type SliderProps = MantineSliderProps;
|
|
||||||
|
|
||||||
const StyledSlider = styled(MantineSlider)`
|
|
||||||
& .mantine-Slider-track {
|
|
||||||
height: 0.5rem;
|
|
||||||
background-color: var(--slider-track-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Slider-bar {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Slider-thumb {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
background: var(--slider-thumb-bg);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Slider-label {
|
|
||||||
padding: 0 1rem;
|
|
||||||
font-size: 1em;
|
|
||||||
color: var(--tooltip-fg);
|
|
||||||
background: var(--tooltip-bg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Slider = ({ ...props }: SliderProps) => {
|
|
||||||
return <StyledSlider {...props} />;
|
|
||||||
};
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import type { SwitchProps as MantineSwitchProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Switch as MantineSwitch } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type SwitchProps = MantineSwitchProps;
|
|
||||||
|
|
||||||
const StyledSwitch = styled(MantineSwitch)`
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
& .mantine-Switch-track {
|
|
||||||
background-color: var(--switch-track-bg);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Switch-input {
|
|
||||||
&:checked + .mantine-Switch-track {
|
|
||||||
background-color: var(--switch-track-enabled-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Switch-thumb {
|
|
||||||
background-color: var(--switch-thumb-bg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Switch = ({ ...props }: SwitchProps) => {
|
|
||||||
return <StyledSwitch {...props} />;
|
|
||||||
};
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { Tabs as MantineTabs, TabsProps as MantineTabsProps, TabsPanelProps } from '@mantine/core';
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type TabsProps = MantineTabsProps;
|
|
||||||
|
|
||||||
const StyledTabs = styled(MantineTabs)`
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
& .mantine-Tabs-tabsList {
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mantine-Tabs-tab {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
background-color: var(--main-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Tabs-panel {
|
|
||||||
padding: 1.5rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Tabs-tab {
|
|
||||||
padding: 1rem;
|
|
||||||
color: var(--btn-subtle-fg);
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--btn-subtle-fg-hover);
|
|
||||||
background: var(--btn-subtle-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
transition:
|
|
||||||
background 0.2s ease-in-out,
|
|
||||||
color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[data-active] {
|
|
||||||
color: var(--btn-subtle-fg);
|
|
||||||
background: none;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Tabs = ({ children, ...props }: TabsProps) => {
|
|
||||||
return <StyledTabs {...props}>{children}</StyledTabs>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Panel = ({ children, ...props }: TabsPanelProps) => {
|
|
||||||
return (
|
|
||||||
<StyledTabs.Panel {...props}>
|
|
||||||
<Suspense fallback={<></>}>{children}</Suspense>
|
|
||||||
</StyledTabs.Panel>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Tabs.List = StyledTabs.List;
|
|
||||||
Tabs.Panel = Panel;
|
|
||||||
Tabs.Tab = StyledTabs.Tab;
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import type { TitleProps as MantineTitleProps } from '@mantine/core';
|
|
||||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { createPolymorphicComponent, Title as MantineHeader } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { textEllipsis } from '/@/renderer/styles';
|
|
||||||
|
|
||||||
type MantineTextTitleDivProps = ComponentPropsWithoutRef<'div'> & MantineTitleProps;
|
|
||||||
|
|
||||||
interface TextTitleProps extends MantineTextTitleDivProps {
|
|
||||||
$link?: boolean;
|
|
||||||
$noSelect?: boolean;
|
|
||||||
$secondary?: boolean;
|
|
||||||
children?: ReactNode;
|
|
||||||
overflow?: 'hidden' | 'visible';
|
|
||||||
to?: string;
|
|
||||||
weight?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
|
|
||||||
overflow: ${(props) => props.overflow};
|
|
||||||
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
|
||||||
cursor: ${(props) => props.$link && 'cursor'};
|
|
||||||
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
|
||||||
transition: color 0.2s ease-in-out;
|
|
||||||
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: ${(props) => props.$link && 'var(--main-fg)'};
|
|
||||||
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const _TextTitle = ({ $noSelect, $secondary, children, overflow, ...rest }: TextTitleProps) => {
|
|
||||||
return (
|
|
||||||
<StyledTextTitle
|
|
||||||
$noSelect={$noSelect}
|
|
||||||
$secondary={$secondary}
|
|
||||||
overflow={overflow}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledTextTitle>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TextTitle = createPolymorphicComponent<'div', TextTitleProps>(_TextTitle);
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import type { Font } from '/@/renderer/styles';
|
|
||||||
import type { TextProps as MantineTextProps } from '@mantine/core';
|
|
||||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { createPolymorphicComponent, Text as MantineText } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { textEllipsis } from '/@/renderer/styles';
|
|
||||||
|
|
||||||
type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineTextProps;
|
|
||||||
|
|
||||||
interface TextProps extends MantineTextDivProps {
|
|
||||||
$link?: boolean;
|
|
||||||
$noSelect?: boolean;
|
|
||||||
$secondary?: boolean;
|
|
||||||
children?: ReactNode;
|
|
||||||
font?: Font;
|
|
||||||
overflow?: 'hidden' | 'visible';
|
|
||||||
to?: string;
|
|
||||||
weight?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledText = styled(MantineText)<TextProps>`
|
|
||||||
overflow: ${(props) => props.overflow};
|
|
||||||
font-family: ${(props) => props.font};
|
|
||||||
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
|
||||||
cursor: ${(props) => props.$link && 'cursor'};
|
|
||||||
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
|
||||||
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: ${(props) => props.$link && 'var(--main-fg)'};
|
|
||||||
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const _Text = ({ $noSelect, $secondary, children, font, overflow, ...rest }: TextProps) => {
|
|
||||||
return (
|
|
||||||
<StyledText
|
|
||||||
$noSelect={$noSelect}
|
|
||||||
$secondary={$secondary}
|
|
||||||
font={font}
|
|
||||||
overflow={overflow}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledText>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Text = createPolymorphicComponent<'div', TextProps>(_Text);
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
|
|
||||||
|
|
||||||
import {
|
|
||||||
cleanNotifications,
|
|
||||||
cleanNotificationsQueue,
|
|
||||||
hideNotification,
|
|
||||||
showNotification,
|
|
||||||
updateNotification,
|
|
||||||
} from '@mantine/notifications';
|
|
||||||
|
|
||||||
interface NotificationProps extends MantineNotificationProps {
|
|
||||||
type?: 'error' | 'info' | 'success' | 'warning';
|
|
||||||
}
|
|
||||||
|
|
||||||
const showToast = ({ type, ...props }: NotificationProps) => {
|
|
||||||
const color =
|
|
||||||
type === 'success'
|
|
||||||
? 'var(--success-color)'
|
|
||||||
: type === 'warning'
|
|
||||||
? 'var(--warning-color)'
|
|
||||||
: type === 'error'
|
|
||||||
? 'var(--danger-color)'
|
|
||||||
: 'var(--primary-color)';
|
|
||||||
|
|
||||||
const defaultTitle =
|
|
||||||
type === 'success'
|
|
||||||
? 'Success'
|
|
||||||
: type === 'warning'
|
|
||||||
? 'Warning'
|
|
||||||
: type === 'error'
|
|
||||||
? 'Error'
|
|
||||||
: 'Info';
|
|
||||||
|
|
||||||
const defaultDuration = type === 'error' ? 5000 : 2000;
|
|
||||||
|
|
||||||
return showNotification({
|
|
||||||
autoClose: defaultDuration,
|
|
||||||
styles: () => ({
|
|
||||||
closeButton: {
|
|
||||||
'&:hover': {
|
|
||||||
background: 'transparent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
color: 'var(--toast-description-fg)',
|
|
||||||
fontSize: '1rem',
|
|
||||||
},
|
|
||||||
loader: {
|
|
||||||
margin: '1rem',
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
'&::before': { backgroundColor: color },
|
|
||||||
background: 'var(--toast-bg)',
|
|
||||||
border: '2px solid var(--generic-border-color)',
|
|
||||||
bottom: '90px',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
color: 'var(--toast-title-fg)',
|
|
||||||
fontSize: '1.3rem',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
title: defaultTitle,
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toast = {
|
|
||||||
clean: cleanNotifications,
|
|
||||||
cleanQueue: cleanNotificationsQueue,
|
|
||||||
error: (props: NotificationProps) => showToast({ type: 'error', ...props }),
|
|
||||||
hide: hideNotification,
|
|
||||||
info: (props: NotificationProps) => showToast({ type: 'info', ...props }),
|
|
||||||
show: showToast,
|
|
||||||
success: (props: NotificationProps) => showToast({ type: 'success', ...props }),
|
|
||||||
update: updateNotification,
|
|
||||||
warn: (props: NotificationProps) => showToast({ type: 'warning', ...props }),
|
|
||||||
};
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import type { TooltipProps } from '@mantine/core';
|
|
||||||
|
|
||||||
import { Tooltip as MantineTooltip } from '@mantine/core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledTooltip = styled(MantineTooltip)`
|
|
||||||
& .mantine-Tooltip-tooltip {
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
|
||||||
return (
|
|
||||||
<StyledTooltip
|
|
||||||
multiline
|
|
||||||
pl={10}
|
|
||||||
pr={10}
|
|
||||||
py={5}
|
|
||||||
radius="xs"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
background: 'var(--tooltip-bg)',
|
|
||||||
boxShadow: '4px 4px 10px 0px rgba(0,0,0,0.2)',
|
|
||||||
color: 'var(--tooltip-fg)',
|
|
||||||
fontSize: '1.1rem',
|
|
||||||
fontWeight: 550,
|
|
||||||
maxWidth: '250px',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
transitionProps={{
|
|
||||||
duration: 250,
|
|
||||||
transition: 'fade',
|
|
||||||
}}
|
|
||||||
withinPortal
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledTooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 2rem);
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--theme-colors-surface-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: lighten(var(--theme-colors-surface), 3%);
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover * {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
content: '';
|
||||||
|
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container.is-favorite {
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: -50px;
|
||||||
|
left: -50px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
background-color: var(--theme-colors-primary-filled);
|
||||||
|
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: var(--theme-image-fit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-container {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { Center, Stack } from '@mantine/core';
|
import clsx from 'clsx';
|
||||||
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
import { useState } from 'react';
|
||||||
import { generatePath, useNavigate } from 'react-router-dom';
|
import { generatePath, useNavigate } from 'react-router-dom';
|
||||||
import { SimpleImg } from 'react-simple-img';
|
|
||||||
import { ListChildComponentProps } from 'react-window';
|
import { ListChildComponentProps } from 'react-window';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { CardRows } from '/@/renderer/components/card';
|
import styles from './default-card.module.css';
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
|
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||||
|
import { Image } from '/@/shared/components/image/image';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
|
|
@ -39,105 +41,6 @@ interface BaseGridCardProps {
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultCardContainer = styled.div<{ $isHidden?: boolean; $itemGap: number }>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 2rem);
|
|
||||||
margin: ${({ $itemGap }) => $itemGap}px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--card-default-bg);
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--card-default-bg-hover);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InnerCardContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .card-controls {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover * {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--placeholder-bg);
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: '';
|
|
||||||
user-select: none;
|
|
||||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
${(props) =>
|
|
||||||
props.$isFavorite &&
|
|
||||||
`
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
box-shadow: 0 0 10px 8px rgba(0, 0, 0, 80%);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
content: '';
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100% !important;
|
|
||||||
max-height: 100%;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: var(--image-fit);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailContainer = styled.div`
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DefaultCard = ({
|
export const DefaultCard = ({
|
||||||
columnIndex,
|
columnIndex,
|
||||||
controls,
|
controls,
|
||||||
|
|
@ -147,6 +50,8 @@ export const DefaultCard = ({
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const path = generatePath(
|
const path = generatePath(
|
||||||
controls.route.route as string,
|
controls.route.route as string,
|
||||||
|
|
@ -158,101 +63,68 @@ export const DefaultCard = ({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Placeholder = RiAlbumFill;
|
|
||||||
|
|
||||||
switch (controls.itemType) {
|
|
||||||
case LibraryItem.ALBUM:
|
|
||||||
Placeholder = RiAlbumFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
|
||||||
Placeholder = RiUserVoiceFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.ARTIST:
|
|
||||||
Placeholder = RiUserVoiceFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.PLAYLIST:
|
|
||||||
Placeholder = RiPlayListFill;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Placeholder = RiAlbumFill;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultCardContainer
|
<div
|
||||||
$itemGap={controls.itemGap}
|
className={clsx(styles.container, isHidden && styles.isHidden)}
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
onClick={() => navigate(path)}
|
onClick={() => navigate(path)}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
style={{
|
||||||
|
margin: controls.itemGap,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<InnerCardContainer>
|
<div className={styles.innerContainer}>
|
||||||
<ImageContainer $isFavorite={data?.userFavorite}>
|
<div
|
||||||
{data?.imageUrl ? (
|
className={clsx(
|
||||||
<Image
|
styles.imageContainer,
|
||||||
importance="auto"
|
data?.userFavorite && styles.isFavorite,
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
|
||||||
src={data?.imageUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-default-radius)',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Placeholder
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className={styles.image}
|
||||||
|
src={data?.imageUrl}
|
||||||
|
/>
|
||||||
<GridCardControls
|
<GridCardControls
|
||||||
handleFavorite={controls.handleFavorite}
|
handleFavorite={controls.handleFavorite}
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
isHovered={isHovered}
|
||||||
itemData={data}
|
itemData={data}
|
||||||
itemType={controls.itemType}
|
itemType={controls.itemType}
|
||||||
resetInfiniteLoaderCache={controls.resetInfiniteLoaderCache}
|
resetInfiniteLoaderCache={controls.resetInfiniteLoaderCache}
|
||||||
/>
|
/>
|
||||||
</ImageContainer>
|
</div>
|
||||||
<DetailContainer>
|
<div className={styles.detailContainer}>
|
||||||
<CardRows
|
<CardRows
|
||||||
data={data}
|
data={data}
|
||||||
rows={controls.cardRows}
|
rows={controls.cardRows}
|
||||||
/>
|
/>
|
||||||
</DetailContainer>
|
</div>
|
||||||
</InnerCardContainer>
|
</div>
|
||||||
</DefaultCardContainer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultCardContainer
|
<div
|
||||||
$isHidden={isHidden}
|
className={clsx(styles.container, isHidden && styles.isHidden)}
|
||||||
$itemGap={controls.itemGap}
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
|
style={{
|
||||||
|
margin: controls.itemGap,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<InnerCardContainer>
|
<div className={styles.innerContainer}>
|
||||||
<ImageContainer>
|
<div className={styles.imageContainer}>
|
||||||
<Skeleton
|
<Skeleton className={styles.image} />
|
||||||
radius="sm"
|
</div>
|
||||||
visible
|
<div className={styles.detailContainer}>
|
||||||
/>
|
<Stack gap="xs">
|
||||||
</ImageContainer>
|
|
||||||
<DetailContainer>
|
|
||||||
<Stack spacing="sm">
|
|
||||||
{(controls?.cardRows || []).map((row, index) => (
|
{(controls?.cardRows || []).map((row, index) => (
|
||||||
<Skeleton
|
<Skeleton key={`${index}-${columnIndex}-${row.arrayProperty}`} />
|
||||||
height={14}
|
|
||||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
|
||||||
radius="sm"
|
|
||||||
visible
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</DetailContainer>
|
</div>
|
||||||
</InnerCardContainer>
|
</div>
|
||||||
</DefaultCardContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
.play-button {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: var(--theme-colors-white);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
transition: scale 0.1s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--theme-colors-white);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--theme-colors-black);
|
||||||
|
stroke: var(--theme-colors-black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
transition: scale 0.2s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-controls-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-banner {
|
||||||
|
position: absolute;
|
||||||
|
top: -50px;
|
||||||
|
left: -50px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
background-color: var(--theme-colors-primary-filled);
|
||||||
|
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-wrapper {
|
||||||
|
svg {
|
||||||
|
fill: var(--theme-colors-primary-filled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% / 3);
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import type { UnstyledButtonProps } from '@mantine/core';
|
import clsx from 'clsx';
|
||||||
|
import { MouseEvent, useState } from 'react';
|
||||||
|
|
||||||
import React, { MouseEvent, useState } from 'react';
|
import styles from './grid-card-controls.module.css';
|
||||||
import { RiHeartFill, RiHeartLine, RiMoreFill, RiPlayFill } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { _Button } from '/@/renderer/components/button';
|
|
||||||
import {
|
import {
|
||||||
ALBUM_CONTEXT_MENU_ITEMS,
|
ALBUM_CONTEXT_MENU_ITEMS,
|
||||||
ARTIST_CONTEXT_MENU_ITEMS,
|
ARTIST_CONTEXT_MENU_ITEMS,
|
||||||
|
|
@ -12,105 +10,16 @@ import {
|
||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||||
|
|
||||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
|
||||||
|
|
||||||
const PlayButton = styled.button<PlayButtonType>`
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: rgb(255 255 255);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
transition: scale 0.1s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: rgb(0 0 0);
|
|
||||||
stroke: rgb(0 0 0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SecondaryButton = styled(_Button)`
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
transition: scale 0.2s linear;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GridCardControlsContainer = styled.div<{ $isFavorite?: boolean }>`
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FavoriteBanner = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
pointer-events: none;
|
|
||||||
content: '';
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ControlsRow = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% / 3);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BottomControls = styled(ControlsRow)`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
|
||||||
svg {
|
|
||||||
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const GridCardControls = ({
|
export const GridCardControls = ({
|
||||||
handleFavorite,
|
handleFavorite,
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
|
isHovered,
|
||||||
itemData,
|
itemData,
|
||||||
itemType,
|
itemType,
|
||||||
resetInfiniteLoaderCache,
|
resetInfiniteLoaderCache,
|
||||||
|
|
@ -122,6 +31,7 @@ export const GridCardControls = ({
|
||||||
serverId: string;
|
serverId: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
|
isHovered?: boolean;
|
||||||
itemData: any;
|
itemData: any;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
resetInfiniteLoaderCache?: () => void;
|
resetInfiniteLoaderCache?: () => void;
|
||||||
|
|
@ -168,50 +78,46 @@ export const GridCardControls = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFavorite ? <FavoriteBanner /> : null}
|
{isFavorite ? <div className={styles.favoriteBanner} /> : null}
|
||||||
<GridCardControlsContainer
|
{isHovered && (
|
||||||
$isFavorite
|
<div className={clsx(styles.gridCardControlsContainer)}>
|
||||||
className="card-controls"
|
<Button
|
||||||
>
|
classNames={{ root: styles.playButton }}
|
||||||
<PlayButton onClick={handlePlay}>
|
onClick={handlePlay}
|
||||||
<RiPlayFill size={25} />
|
variant="filled"
|
||||||
</PlayButton>
|
|
||||||
<BottomControls>
|
|
||||||
{itemType !== LibraryItem.PLAYLIST && (
|
|
||||||
<SecondaryButton
|
|
||||||
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
|
||||||
p={5}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
|
||||||
{isFavorite ? (
|
|
||||||
<RiHeartFill size={20} />
|
|
||||||
) : (
|
|
||||||
<RiHeartLine
|
|
||||||
color="white"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FavoriteWrapper>
|
|
||||||
</SecondaryButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SecondaryButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleContextMenu(e, [itemData]);
|
|
||||||
}}
|
|
||||||
p={5}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
>
|
||||||
<RiMoreFill
|
<Icon
|
||||||
color="white"
|
icon="mediaPlay"
|
||||||
size={20}
|
size="xl"
|
||||||
/>
|
/>
|
||||||
</SecondaryButton>
|
</Button>
|
||||||
</BottomControls>
|
<div className={styles.bottomControls}>
|
||||||
</GridCardControlsContainer>
|
{itemType !== LibraryItem.PLAYLIST && (
|
||||||
|
<ActionIcon
|
||||||
|
classNames={{ root: styles.secondaryButton }}
|
||||||
|
icon={isFavorite ? 'favorite' : 'favorite'}
|
||||||
|
iconProps={{
|
||||||
|
fill: isFavorite ? 'primary' : undefined,
|
||||||
|
}}
|
||||||
|
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
||||||
|
size="sm"
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ActionIcon
|
||||||
|
classNames={{ root: styles.secondaryButton }}
|
||||||
|
icon="ellipsisHorizontal"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleContextMenu(e, [itemData]);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&:global(.card-controls) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-container {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
content: '';
|
||||||
|
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container.is-favorite {
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: -50px;
|
||||||
|
left: -50px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
background-color: var(--theme-colors-primary-filled);
|
||||||
|
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: var(--theme-image-fit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-container {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { Center, Stack } from '@mantine/core';
|
import clsx from 'clsx';
|
||||||
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
import { useState } from 'react';
|
||||||
import { generatePath, useNavigate } from 'react-router-dom';
|
import { generatePath, useNavigate } from 'react-router-dom';
|
||||||
import { SimpleImg } from 'react-simple-img';
|
|
||||||
import { ListChildComponentProps } from 'react-window';
|
import { ListChildComponentProps } from 'react-window';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { CardRows } from '/@/renderer/components/card';
|
import styles from './poster-card.module.css';
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
|
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||||
|
import { Image } from '/@/shared/components/image/image';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
|
|
@ -39,93 +41,6 @@ interface BaseGridCardProps {
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PosterCardContainer = styled.div<{ $isHidden?: boolean; $itemGap: number }>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: ${({ $itemGap }) => $itemGap}px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LinkContainer = styled.div`
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--card-default-bg);
|
|
||||||
border-radius: var(--card-poster-radius);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: '';
|
|
||||||
user-select: none;
|
|
||||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.$isFavorite &&
|
|
||||||
`
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
box-shadow: 0 0 10px 8px rgba(0, 0, 0, 80%);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
content: '';
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .card-controls {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100% !important;
|
|
||||||
max-height: 100%;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: var(--image-fit);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailContainer = styled.div`
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const PosterCard = ({
|
export const PosterCard = ({
|
||||||
columnIndex,
|
columnIndex,
|
||||||
controls,
|
controls,
|
||||||
|
|
@ -135,6 +50,8 @@ export const PosterCard = ({
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const path = generatePath(
|
const path = generatePath(
|
||||||
controls.route.route as string,
|
controls.route.route as string,
|
||||||
|
|
@ -146,97 +63,68 @@ export const PosterCard = ({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Placeholder = RiAlbumFill;
|
|
||||||
|
|
||||||
switch (controls.itemType) {
|
|
||||||
case LibraryItem.ALBUM:
|
|
||||||
Placeholder = RiAlbumFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
|
||||||
Placeholder = RiUserVoiceFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.ARTIST:
|
|
||||||
Placeholder = RiUserVoiceFill;
|
|
||||||
break;
|
|
||||||
case LibraryItem.PLAYLIST:
|
|
||||||
Placeholder = RiPlayListFill;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Placeholder = RiAlbumFill;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PosterCardContainer
|
<div
|
||||||
$itemGap={controls.itemGap}
|
className={styles.container}
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
style={{
|
||||||
|
margin: controls.itemGap,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LinkContainer onClick={() => navigate(path)}>
|
<div
|
||||||
<ImageContainer $isFavorite={data?.userFavorite}>
|
className={styles.linkContainer}
|
||||||
{data?.imageUrl ? (
|
onClick={() => navigate(path)}
|
||||||
<Image
|
>
|
||||||
importance="auto"
|
<div
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
className={`${styles.imageContainer} ${data?.userFavorite ? styles.isFavorite : ''}`}
|
||||||
src={data?.imageUrl}
|
>
|
||||||
/>
|
<Image
|
||||||
) : (
|
className={styles.image}
|
||||||
<Center
|
src={data?.imageUrl}
|
||||||
sx={{
|
/>
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-default-radius)',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Placeholder
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<GridCardControls
|
<GridCardControls
|
||||||
handleFavorite={controls.handleFavorite}
|
handleFavorite={controls.handleFavorite}
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
isHovered={isHovered}
|
||||||
itemData={data}
|
itemData={data}
|
||||||
itemType={controls.itemType}
|
itemType={controls.itemType}
|
||||||
resetInfiniteLoaderCache={controls.resetInfiniteLoaderCache}
|
resetInfiniteLoaderCache={controls.resetInfiniteLoaderCache}
|
||||||
/>
|
/>
|
||||||
</ImageContainer>
|
</div>
|
||||||
</LinkContainer>
|
</div>
|
||||||
<DetailContainer>
|
<div className={styles.detailContainer}>
|
||||||
<CardRows
|
<CardRows
|
||||||
data={data}
|
data={data}
|
||||||
rows={controls.cardRows}
|
rows={controls.cardRows}
|
||||||
/>
|
/>
|
||||||
</DetailContainer>
|
</div>
|
||||||
</PosterCardContainer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PosterCardContainer
|
<div
|
||||||
$isHidden={isHidden}
|
className={clsx(styles.container, isHidden && styles.hidden)}
|
||||||
$itemGap={controls.itemGap}
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
|
style={{
|
||||||
|
margin: controls.itemGap,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Skeleton
|
<div className={styles.imageContainer}>
|
||||||
radius="sm"
|
<Skeleton className={styles.image} />
|
||||||
visible
|
</div>
|
||||||
>
|
<div className={styles.detailContainer}>
|
||||||
<ImageContainer />
|
<Stack gap="xs">
|
||||||
</Skeleton>
|
|
||||||
<DetailContainer>
|
|
||||||
<Stack spacing="sm">
|
|
||||||
{(controls?.cardRows || []).map((row, index) => (
|
{(controls?.cardRows || []).map((row, index) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height={14}
|
className={styles.row}
|
||||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
||||||
radius="sm"
|
|
||||||
visible
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</DetailContainer>
|
</div>
|
||||||
</PosterCardContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
.virtual-grid-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-grid-auto-sizer-container {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,8 @@ import type { FixedSizeListProps } from 'react-window';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import memoize from 'memoize-one';
|
import memoize from 'memoize-one';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
import styles from './virtual-grid-wrapper.module.css';
|
||||||
|
|
||||||
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
|
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
@ -128,12 +129,14 @@ export const VirtualGridWrapper = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VirtualGridContainer = styled.div`
|
interface VirtualGridContainerProps {
|
||||||
display: flex;
|
children: React.ReactNode;
|
||||||
flex-direction: column;
|
}
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const VirtualGridAutoSizerContainer = styled.div`
|
export const VirtualGridContainer = ({ children }: VirtualGridContainerProps) => {
|
||||||
flex: 1;
|
return <div className={styles.virtualGridContainer}>{children}</div>;
|
||||||
`;
|
};
|
||||||
|
|
||||||
|
export const VirtualGridAutoSizerContainer = ({ children }: VirtualGridContainerProps) => {
|
||||||
|
return <div className={styles.virtualGridAutoSizerContainer}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
import { RiMoreFill } from 'react-icons/ri';
|
|
||||||
|
|
||||||
import { Button } from '/@/renderer/components/button';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
|
||||||
export const ActionsCell = ({ api, context }: ICellRendererParams) => {
|
export const ActionsCell = ({ api, context }: ICellRendererParams) => {
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="center">
|
<CellContainer position="center">
|
||||||
<Button
|
<ActionIcon
|
||||||
compact
|
icon="ellipsisHorizontal"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
context.onCellContextMenu(undefined, api, e);
|
context.onCellContextMenu(undefined, api, e);
|
||||||
}}
|
}}
|
||||||
|
size="sm"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
/>
|
||||||
<RiMoreFill />
|
|
||||||
</Button>
|
|
||||||
</CellContainer>
|
</CellContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,16 @@ import React from 'react';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Separator } from '/@/renderer/components/separator';
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { Separator } from '/@/shared/components/separator/separator';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height="1rem"
|
height="1rem"
|
||||||
width="80%"
|
width="80%"
|
||||||
|
|
@ -24,9 +24,9 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
@ -35,9 +35,9 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
{index > 0 && <Separator />}
|
{index > 0 && <Separator />}
|
||||||
{item.id ? (
|
{item.id ? (
|
||||||
<Text
|
<Text
|
||||||
$link
|
|
||||||
$secondary
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink
|
||||||
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
|
|
@ -48,7 +48,7 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,16 @@ import React from 'react';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Separator } from '/@/renderer/components/separator';
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { Separator } from '/@/shared/components/separator/separator';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height="1rem"
|
height="1rem"
|
||||||
width="80%"
|
width="80%"
|
||||||
|
|
@ -24,9 +24,9 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
@ -35,9 +35,9 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
{index > 0 && <Separator />}
|
{index > 0 && <Separator />}
|
||||||
{item.id ? (
|
{item.id ? (
|
||||||
<Text
|
<Text
|
||||||
$link
|
|
||||||
$secondary
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink
|
||||||
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
|
|
@ -48,7 +48,7 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
.play-button {
|
||||||
|
position: absolute;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background: var(--theme-colors-white);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: scale 0.1s ease-in-out;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--theme-colors-black);
|
||||||
|
fill: var(--theme-colors-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--theme-colors-white);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-controls-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
@ -1,62 +1,21 @@
|
||||||
import type { UnstyledButtonProps } from '@mantine/core';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import React, { MouseEvent } from 'react';
|
import styles from './combined-title-cell-controls.module.css';
|
||||||
import { RiPlayFill } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
|
||||||
|
|
||||||
const PlayButton = styled.button<PlayButtonType>`
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background-color: rgb(255 255 255);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: scale 0.1s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: rgb(0 0 0);
|
|
||||||
stroke: rgb(0 0 0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ListConverControlsContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ListCoverControls = ({
|
export const ListCoverControls = ({
|
||||||
|
className,
|
||||||
context,
|
context,
|
||||||
itemData,
|
itemData,
|
||||||
itemType,
|
itemType,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
}: {
|
}: {
|
||||||
|
className?: string;
|
||||||
context: Record<string, any>;
|
context: Record<string, any>;
|
||||||
itemData: any;
|
itemData: any;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
|
|
@ -66,7 +25,7 @@ export const ListCoverControls = ({
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const isQueue = Boolean(context?.isQueue);
|
const isQueue = Boolean(context?.isQueue);
|
||||||
|
|
||||||
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
const handlePlay = async (e: React.MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
|
@ -88,12 +47,12 @@ export const ListCoverControls = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={clsx(styles.listControlsContainer, className)}>
|
||||||
<ListConverControlsContainer className="card-controls">
|
<ActionIcon
|
||||||
<PlayButton onClick={isQueue ? handlePlayFromQueue : handlePlay}>
|
classNames={{ root: styles.playButton }}
|
||||||
<RiPlayFill size={20} />
|
icon="mediaPlay"
|
||||||
</PlayButton>
|
onClick={isQueue ? handlePlayFromQueue : handlePlay}
|
||||||
</ListConverControlsContainer>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
.cell-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'image info';
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.play-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
grid-area: image;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-area: info;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-wrapper > div {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-metadata {
|
||||||
|
height: var(--theme-font-size-md);
|
||||||
|
}
|
||||||
|
|
@ -1,67 +1,19 @@
|
||||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
import { Center } from '@mantine/core';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { RiAlbumFill } from 'react-icons/ri';
|
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { SimpleImg } from 'react-simple-img';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
import styles from './combined-title-cell.module.css';
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { ListCoverControls } from '/@/renderer/components/virtual-table/cells/combined-title-cell-controls';
|
import { ListCoverControls } from '/@/renderer/components/virtual-table/cells/combined-title-cell-controls';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||||
|
import { Image } from '/@/shared/components/image/image';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
import { AlbumArtist, Artist } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const CellContainer = styled(motion.div)<{ height: number }>`
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'image info';
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
grid-template-columns: ${(props) => props.height}px minmax(0, 1fr);
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.card-controls {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageWrapper = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
grid-area: image;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MetadataWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
grid-area: info;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledImage = styled(SimpleImg)`
|
|
||||||
img {
|
|
||||||
object-fit: var(--image-fit);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CombinedTitleCell = ({
|
export const CombinedTitleCell = ({
|
||||||
context,
|
context,
|
||||||
data,
|
data,
|
||||||
|
|
@ -76,60 +28,55 @@ export const CombinedTitleCell = ({
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return (
|
return (
|
||||||
<CellContainer height={node.rowHeight || 40}>
|
<div
|
||||||
<Skeleton>
|
className={styles.cellContainer}
|
||||||
<ImageWrapper />
|
style={{ gridTemplateColumns: `${node.rowHeight || 40}px minmax(0, 1fr)` }}
|
||||||
</Skeleton>
|
>
|
||||||
<MetadataWrapper>
|
<div
|
||||||
<Skeleton
|
className={styles.imageWrapper}
|
||||||
height="1rem"
|
style={{
|
||||||
width="80%"
|
height: `${(node.rowHeight || 40) - 10}px`,
|
||||||
/>
|
width: `${(node.rowHeight || 40) - 10}px`,
|
||||||
<Skeleton
|
}}
|
||||||
height="1rem"
|
>
|
||||||
mt="0.5rem"
|
<Skeleton className={styles.image} />
|
||||||
width="60%"
|
</div>
|
||||||
/>
|
<Skeleton
|
||||||
</MetadataWrapper>
|
className={styles.skeletonMetadata}
|
||||||
</CellContainer>
|
height="1rem"
|
||||||
|
width="80%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer height={node.rowHeight || 40}>
|
<div
|
||||||
<ImageWrapper>
|
className={styles.cellContainer}
|
||||||
{value.imageUrl ? (
|
style={{ gridTemplateColumns: `${node.rowHeight || 40}px minmax(0, 1fr)` }}
|
||||||
<StyledImage
|
>
|
||||||
alt="cover"
|
<div
|
||||||
height={(node.rowHeight || 40) - 10}
|
className={styles.imageWrapper}
|
||||||
placeholder={value.imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
style={{
|
||||||
src={value.imageUrl}
|
height: `${(node.rowHeight || 40) - 10}px`,
|
||||||
style={{}}
|
width: `${(node.rowHeight || 40) - 10}px`,
|
||||||
width={(node.rowHeight || 40) - 10}
|
}}
|
||||||
/>
|
>
|
||||||
) : (
|
<Image
|
||||||
<Center
|
alt="cover"
|
||||||
sx={{
|
className={styles.image}
|
||||||
background: 'var(--placeholder-bg)',
|
src={value.imageUrl}
|
||||||
borderRadius: 'var(--card-default-radius)',
|
/>
|
||||||
height: `${(node.rowHeight || 40) - 10}px`,
|
|
||||||
width: `${(node.rowHeight || 40) - 10}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiAlbumFill
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<ListCoverControls
|
<ListCoverControls
|
||||||
|
className={styles.playButton}
|
||||||
context={context}
|
context={context}
|
||||||
itemData={value}
|
itemData={value}
|
||||||
itemType={context.itemType}
|
itemType={context.itemType}
|
||||||
uniqueId={data?.uniqueId}
|
uniqueId={data?.uniqueId}
|
||||||
/>
|
/>
|
||||||
</ImageWrapper>
|
</div>
|
||||||
<MetadataWrapper>
|
<div className={styles.metadataWrapper}>
|
||||||
<Text
|
<Text
|
||||||
className="current-song-child"
|
className="current-song-child"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
|
|
@ -138,7 +85,7 @@ export const CombinedTitleCell = ({
|
||||||
{value.name}
|
{value.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
@ -148,12 +95,12 @@ export const CombinedTitleCell = ({
|
||||||
{index > 0 ? SEPARATOR_STRING : null}
|
{index > 0 ? SEPARATOR_STRING : null}
|
||||||
{artist.id ? (
|
{artist.id ? (
|
||||||
<Text
|
<Text
|
||||||
$link
|
|
||||||
$secondary
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink
|
||||||
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
sx={{ width: 'fit-content' }}
|
style={{ width: 'fit-content' }}
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
albumArtistId: artist.id,
|
albumArtistId: artist.id,
|
||||||
})}
|
})}
|
||||||
|
|
@ -162,10 +109,10 @@ export const CombinedTitleCell = ({
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
sx={{ width: 'fit-content' }}
|
style={{ width: 'fit-content' }}
|
||||||
>
|
>
|
||||||
{artist.name}
|
{artist.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -173,10 +120,10 @@ export const CombinedTitleCell = ({
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text $secondary>—</Text>
|
<Text isMuted>—</Text>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</MetadataWrapper>
|
</div>
|
||||||
</CellContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
import { RiHeartFill, RiHeartLine } from 'react-icons/ri';
|
|
||||||
|
|
||||||
import { Button } from '/@/renderer/components/button';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
|
||||||
export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
|
export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
|
||||||
const createMutation = useCreateFavorite({});
|
const createMutation = useCreateFavorite({});
|
||||||
|
|
@ -47,21 +45,16 @@ export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="center">
|
<CellContainer position="center">
|
||||||
<Button
|
<ActionIcon
|
||||||
compact
|
icon="favorite"
|
||||||
onClick={handleToggleFavorite}
|
iconProps={{
|
||||||
sx={{
|
fill: !value ? undefined : 'primary',
|
||||||
svg: {
|
|
||||||
fill: !value
|
|
||||||
? 'var(--main-fg-secondary) !important'
|
|
||||||
: 'var(--primary-color) !important',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
size="sm"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
/>
|
||||||
{!value ? <RiHeartLine size="1.3em" /> : <RiHeartFill size="1.3em" />}
|
|
||||||
</Button>
|
|
||||||
</CellContainer>
|
</CellContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
import { ICellRendererParams } from '@ag-grid-community/core';
|
import { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
import { Group } from '@mantine/core';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RiCheckboxBlankLine, RiCheckboxLine } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { Button } from '/@/renderer/components/button';
|
import styles from './full-width-disc-cell.module.css';
|
||||||
import { Paper } from '/@/renderer/components/paper';
|
|
||||||
import { getNodesByDiscNumber, setNodeSelection } from '/@/renderer/components/virtual-table/utils';
|
import { getNodesByDiscNumber, setNodeSelection } from '/@/renderer/components/virtual-table/utils';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
const Container = styled(Paper)`
|
import { Group } from '/@/shared/components/group/group';
|
||||||
display: flex;
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
height: 100%;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
|
export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
|
||||||
const [isSelected, setIsSelected] = useState(false);
|
const [isSelected, setIsSelected] = useState(false);
|
||||||
|
|
@ -31,21 +24,20 @@ export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<div className={styles.container}>
|
||||||
<Group
|
<Group
|
||||||
position="apart"
|
justify="space-between"
|
||||||
w="100%"
|
w="100%"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
compact
|
leftSection={isSelected ? <Icon icon="squareCheck" /> : <Icon icon="square" />}
|
||||||
leftIcon={isSelected ? <RiCheckboxLine /> : <RiCheckboxBlankLine />}
|
|
||||||
onClick={handleToggleDiscNodes}
|
onClick={handleToggleDiscNodes}
|
||||||
size="md"
|
size="compact-md"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{data.name}
|
{data.name}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
.cell-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-container.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-container.center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-container.left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,12 @@
|
||||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
import styles from './generic-cell.module.css';
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
|
|
||||||
export const CellContainer = styled.div<{ $position?: 'center' | 'left' | 'right' }>`
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
display: flex;
|
import { Text } from '/@/shared/components/text/text';
|
||||||
align-items: center;
|
|
||||||
justify-content: ${(props) =>
|
|
||||||
props.$position === 'right'
|
|
||||||
? 'flex-end'
|
|
||||||
: props.$position === 'center'
|
|
||||||
? 'center'
|
|
||||||
: 'flex-start'};
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
array?: boolean;
|
array?: boolean;
|
||||||
|
|
@ -36,7 +24,7 @@ export const GenericCell = (
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return (
|
return (
|
||||||
<CellContainer $position={position || 'left'}>
|
<CellContainer position={position || 'left'}>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height="1rem"
|
height="1rem"
|
||||||
width="80%"
|
width="80%"
|
||||||
|
|
@ -46,12 +34,12 @@ export const GenericCell = (
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position={position || 'left'}>
|
<CellContainer position={position || 'left'}>
|
||||||
{isLink ? (
|
{isLink ? (
|
||||||
<Text
|
<Text
|
||||||
$link={isLink}
|
|
||||||
$secondary={!primary}
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink={isLink}
|
||||||
|
isMuted={!primary}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
to={displayedValue.link}
|
to={displayedValue.link}
|
||||||
|
|
@ -60,8 +48,8 @@ export const GenericCell = (
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
$noSelect={false}
|
isMuted={!primary}
|
||||||
$secondary={!primary}
|
isNoSelect={false}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
@ -71,3 +59,24 @@ export const GenericCell = (
|
||||||
</CellContainer>
|
</CellContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CellContainer = ({
|
||||||
|
children,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
position: 'center' | 'left' | 'right';
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx({
|
||||||
|
[styles.cellContainer]: true,
|
||||||
|
[styles.center]: position === 'center',
|
||||||
|
[styles.left]: position === 'left' || !position,
|
||||||
|
[styles.right]: position === 'right',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,17 @@ import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Separator } from '/@/renderer/components/separator';
|
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||||
|
import { Separator } from '/@/shared/components/separator/separator';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
export const GenreCell = ({ data, value }: ICellRendererParams) => {
|
export const GenreCell = ({ data, value }: ICellRendererParams) => {
|
||||||
const genrePath = useGenreRoute();
|
const genrePath = useGenreRoute();
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|
@ -22,9 +22,9 @@ export const GenreCell = ({ data, value }: ICellRendererParams) => {
|
||||||
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
|
||||||
{index > 0 && <Separator />}
|
{index > 0 && <Separator />}
|
||||||
<Text
|
<Text
|
||||||
$link
|
|
||||||
$secondary
|
|
||||||
component={Link}
|
component={Link}
|
||||||
|
isLink
|
||||||
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="md"
|
size="md"
|
||||||
to={generatePath(genrePath, { genreId: item.id })}
|
to={generatePath(genrePath, { genreId: item.id })}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||||
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
export const NoteCell = ({ value }: ICellRendererParams) => {
|
export const NoteCell = ({ value }: ICellRendererParams) => {
|
||||||
const formattedValue = useMemo(() => {
|
const formattedValue = useMemo(() => {
|
||||||
|
|
@ -18,7 +18,7 @@ export const NoteCell = ({ value }: ICellRendererParams) => {
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height="1rem"
|
height="1rem"
|
||||||
width="80%"
|
width="80%"
|
||||||
|
|
@ -28,9 +28,9 @@ export const NoteCell = ({ value }: ICellRendererParams) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="left">
|
<CellContainer position="left">
|
||||||
<Text
|
<Text
|
||||||
$secondary
|
isMuted
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{formattedValue}
|
{formattedValue}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
import { Rating } from '/@/renderer/components/rating';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
import { useSetRating } from '/@/renderer/features/shared';
|
import { useSetRating } from '/@/renderer/features/shared';
|
||||||
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
|
|
||||||
export const RatingCell = ({ node, value }: ICellRendererParams) => {
|
export const RatingCell = ({ node, value }: ICellRendererParams) => {
|
||||||
const updateRatingMutation = useSetRating({});
|
const updateRatingMutation = useSetRating({});
|
||||||
|
|
@ -25,7 +25,7 @@ export const RatingCell = ({ node, value }: ICellRendererParams) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="center">
|
<CellContainer position="center">
|
||||||
<Rating
|
<Rating
|
||||||
onChange={handleUpdateRating}
|
onChange={handleUpdateRating}
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||||
|
|
||||||
import { RiPlayFill } from 'react-icons/ri';
|
|
||||||
|
|
||||||
import { Text } from '/@/renderer/components/text';
|
|
||||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
// const AnimatedSvg = () => {
|
// const AnimatedSvg = () => {
|
||||||
// return (
|
// return (
|
||||||
|
|
@ -14,7 +13,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
|
||||||
// >
|
// >
|
||||||
// <g>
|
// <g>
|
||||||
// <rect
|
// <rect
|
||||||
// fill="var(--primary-color)"
|
// fill="var(--theme-colors-primary-filled)"
|
||||||
// height="80"
|
// height="80"
|
||||||
// id="bar-1"
|
// id="bar-1"
|
||||||
// width="12"
|
// width="12"
|
||||||
|
|
@ -33,7 +32,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
|
||||||
// />
|
// />
|
||||||
// </rect>
|
// </rect>
|
||||||
// <rect
|
// <rect
|
||||||
// fill="var(--primary-color)"
|
// fill="var(--theme-colors-primary-filled)"
|
||||||
// height="80"
|
// height="80"
|
||||||
// id="bar-2"
|
// id="bar-2"
|
||||||
// width="12"
|
// width="12"
|
||||||
|
|
@ -52,7 +51,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
|
||||||
// />
|
// />
|
||||||
// </rect>
|
// </rect>
|
||||||
// <rect
|
// <rect
|
||||||
// fill="var(--primary-color)"
|
// fill="var(--theme-colors-primary-filled)"
|
||||||
// height="80"
|
// height="80"
|
||||||
// id="bar-3"
|
// id="bar-3"
|
||||||
// width="12"
|
// width="12"
|
||||||
|
|
@ -71,7 +70,7 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
|
||||||
// />
|
// />
|
||||||
// </rect>
|
// </rect>
|
||||||
// <rect
|
// <rect
|
||||||
// fill="var(--primary-color)"
|
// fill="var(--theme-colors-primary-filled)"
|
||||||
// height="80"
|
// height="80"
|
||||||
// id="bar-4"
|
// id="bar-4"
|
||||||
// width="12"
|
// width="12"
|
||||||
|
|
@ -143,23 +142,28 @@ export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
|
||||||
classList.contains('current-song-cell') || classList.contains('current-playlist-song-cell');
|
classList.contains('current-song-cell') || classList.contains('current-playlist-song-cell');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellContainer $position="right">
|
<CellContainer position="right">
|
||||||
{isPlaying &&
|
{isPlaying && isCurrentSong ? (
|
||||||
(isCurrentSong ? (
|
<Icon
|
||||||
<RiPlayFill
|
fill="primary"
|
||||||
color="var(--primary-color)"
|
icon="mediaPlay"
|
||||||
size="1.2rem"
|
/>
|
||||||
/>
|
) : isCurrentSong ? (
|
||||||
) : null)}
|
<Icon
|
||||||
<Text
|
fill="primary"
|
||||||
$secondary
|
icon="mediaPause"
|
||||||
align="right"
|
/>
|
||||||
className="current-song-child current-song-index"
|
) : (
|
||||||
overflow="hidden"
|
<Text
|
||||||
size="md"
|
className="current-song-child current-song-index"
|
||||||
>
|
isMuted
|
||||||
{value}
|
overflow="hidden"
|
||||||
</Text>
|
size="md"
|
||||||
|
style={{ textAlign: 'right' }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</CellContainer>
|
</CellContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue