mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 06:27:09 +01:00
Lint all files
This commit is contained in:
parent
22af76b4d6
commit
30e52ebb54
170
.eslintrc.js
170
.eslintrc.js
@ -1,90 +1,90 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['erb', 'plugin:typescript-sort-keys/recommended'],
|
extends: ['erb', 'plugin:typescript-sort-keys/recommended'],
|
||||||
ignorePatterns: ['.erb/*', 'server'],
|
ignorePatterns: ['.erb/*', 'server'],
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
createDefaultProgram: true,
|
|
||||||
ecmaVersion: 12,
|
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
project: './tsconfig.json',
|
parserOptions: {
|
||||||
sourceType: 'module',
|
createDefaultProgram: true,
|
||||||
tsconfigRootDir: './',
|
ecmaVersion: 12,
|
||||||
},
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['@typescript-eslint', 'import', 'sort-keys-fix'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/naming-convention': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/no-shadow': ['off'],
|
|
||||||
'default-case': 'off',
|
|
||||||
'import/extensions': 'off',
|
|
||||||
'import/no-absolute-path': 'off',
|
|
||||||
// A temporary hack related to IDE not resolving correct package.json
|
|
||||||
'import/no-extraneous-dependencies': 'off',
|
|
||||||
|
|
||||||
'import/no-unresolved': 'error',
|
|
||||||
'import/order': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
alphabetize: {
|
|
||||||
caseInsensitive: true,
|
|
||||||
order: 'asc',
|
|
||||||
},
|
|
||||||
groups: ['builtin', 'external', 'internal', ['parent', 'sibling']],
|
|
||||||
'newlines-between': 'never',
|
|
||||||
pathGroups: [
|
|
||||||
{
|
|
||||||
group: 'external',
|
|
||||||
pattern: 'react',
|
|
||||||
position: 'before',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pathGroupsExcludedImportTypes: ['react'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'import/prefer-default-export': 'off',
|
|
||||||
'jsx-a11y/click-events-have-key-events': 'off',
|
|
||||||
'jsx-a11y/interactive-supports-focus': 'off',
|
|
||||||
'jsx-a11y/media-has-caption': 'off',
|
|
||||||
'no-await-in-loop': 'off',
|
|
||||||
'no-console': 'off',
|
|
||||||
'no-nested-ternary': 'off',
|
|
||||||
'no-restricted-syntax': 'off',
|
|
||||||
'no-underscore-dangle': 'off',
|
|
||||||
'prefer-destructuring': 'off',
|
|
||||||
'react/jsx-props-no-spreading': 'off',
|
|
||||||
'react/jsx-sort-props': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
callbacksLast: true,
|
|
||||||
ignoreCase: false,
|
|
||||||
noSortAlphabetically: false,
|
|
||||||
reservedFirst: true,
|
|
||||||
shorthandFirst: true,
|
|
||||||
shorthandLast: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'react/no-array-index-key': 'off',
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'react/require-default-props': 'off',
|
|
||||||
'sort-keys-fix/sort-keys-fix': 'warn',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
'import/parsers': {
|
|
||||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
|
||||||
node: {
|
|
||||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
alwaysTryTypes: true,
|
|
||||||
project: './tsconfig.json',
|
project: './tsconfig.json',
|
||||||
},
|
sourceType: 'module',
|
||||||
webpack: {
|
tsconfigRootDir: './',
|
||||||
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
|
},
|
||||||
},
|
plugins: ['@typescript-eslint', 'import', 'sort-keys-fix'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/naming-convention': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': ['off'],
|
||||||
|
'default-case': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-absolute-path': 'off',
|
||||||
|
// A temporary hack related to IDE not resolving correct package.json
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
|
||||||
|
'import/no-unresolved': 'error',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
alphabetize: {
|
||||||
|
caseInsensitive: true,
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
|
groups: ['builtin', 'external', 'internal', ['parent', 'sibling']],
|
||||||
|
'newlines-between': 'never',
|
||||||
|
pathGroups: [
|
||||||
|
{
|
||||||
|
group: 'external',
|
||||||
|
pattern: 'react',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pathGroupsExcludedImportTypes: ['react'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'jsx-a11y/click-events-have-key-events': 'off',
|
||||||
|
'jsx-a11y/interactive-supports-focus': 'off',
|
||||||
|
'jsx-a11y/media-has-caption': 'off',
|
||||||
|
'no-await-in-loop': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'prefer-destructuring': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
ignoreCase: false,
|
||||||
|
noSortAlphabetically: false,
|
||||||
|
reservedFirst: true,
|
||||||
|
shorthandFirst: true,
|
||||||
|
shorthandLast: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'sort-keys-fix/sort-keys-fix': 'warn',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
||||||
|
node: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
webpack: {
|
||||||
|
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
8
.github/ISSUE_TEMPLATE/1-Bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/1-Bug_report.md
vendored
@ -39,7 +39,7 @@ labels: 'bug'
|
|||||||
|
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
|
||||||
- Application version (e.g. v0.1.0) :
|
- Application version (e.g. v0.1.0) :
|
||||||
- Operating System and version (e.g. Windows 10) :
|
- Operating System and version (e.g. Windows 10) :
|
||||||
- Server and version (e.g. Navidrome v0.48.0) :
|
- Server and version (e.g. Navidrome v0.48.0) :
|
||||||
- Node version (if developing locally) :
|
- Node version (if developing locally) :
|
||||||
|
10
.github/config.yml
vendored
10
.github/config.yml
vendored
@ -1,6 +1,6 @@
|
|||||||
requiredHeaders:
|
requiredHeaders:
|
||||||
- Prerequisites
|
- Prerequisites
|
||||||
- Expected Behavior
|
- Expected Behavior
|
||||||
- Current Behavior
|
- Current Behavior
|
||||||
- Possible Solution
|
- Possible Solution
|
||||||
- Your Environment
|
- Your Environment
|
||||||
|
10
.github/stale.yml
vendored
10
.github/stale.yml
vendored
@ -4,14 +4,14 @@ daysUntilStale: 60
|
|||||||
daysUntilClose: 7
|
daysUntilClose: 7
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- discussion
|
- discussion
|
||||||
- security
|
- security
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: wontfix
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
|
||||||
for your contributions.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
58
.github/workflows/publish-linux.yml
vendored
58
.github/workflows/publish-linux.yml
vendored
@ -3,37 +3,37 @@ name: Publish Linux (Manual)
|
|||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install --legacy-peer-deps
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
- name: Publish releases
|
- name: Publish releases
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: nick-invision/retry@v2.8.2
|
uses: nick-invision/retry@v2.8.2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 30
|
timeout_minutes: 30
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
npm run build
|
npm run build
|
||||||
npm exec electron-builder -- --publish always --linux
|
npm exec electron-builder -- --publish always --linux
|
||||||
on_retry_command: npm cache clean --force
|
on_retry_command: npm cache clean --force
|
||||||
|
58
.github/workflows/publish-macos.yml
vendored
58
.github/workflows/publish-macos.yml
vendored
@ -3,37 +3,37 @@ name: Publish Windows and macOS (Manual)
|
|||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install --legacy-peer-deps
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
- name: Publish releases
|
- name: Publish releases
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: nick-invision/retry@v2.8.2
|
uses: nick-invision/retry@v2.8.2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 30
|
timeout_minutes: 30
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
npm run build
|
npm run build
|
||||||
npm exec electron-builder -- --publish always --win --mac
|
npm exec electron-builder -- --publish always --win --mac
|
||||||
on_retry_command: npm cache clean --force
|
on_retry_command: npm cache clean --force
|
||||||
|
102
.github/workflows/publish-pr-comment.yml
vendored
102
.github/workflows/publish-pr-comment.yml
vendored
@ -1,54 +1,54 @@
|
|||||||
name: Comment on pull request
|
name: Comment on pull request
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ['Publish (PR)']
|
workflows: ['Publish (PR)']
|
||||||
types: [completed]
|
types: [completed]
|
||||||
jobs:
|
jobs:
|
||||||
pr_comment:
|
pr_comment:
|
||||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
# This snippet is public-domain, taken from
|
# This snippet is public-domain, taken from
|
||||||
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
||||||
script: |
|
script: |
|
||||||
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
||||||
const {data: comments} = await github.rest.issues.listComments(
|
const {data: comments} = await github.rest.issues.listComments(
|
||||||
{owner, repo, issue_number});
|
{owner, repo, issue_number});
|
||||||
const marker = `<!-- bot: ${purpose} -->`;
|
const marker = `<!-- bot: ${purpose} -->`;
|
||||||
body = marker + "\n" + body;
|
body = marker + "\n" + body;
|
||||||
const existing = comments.filter((c) => c.body.includes(marker));
|
const existing = comments.filter((c) => c.body.includes(marker));
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
const last = existing[existing.length - 1];
|
const last = existing[existing.length - 1];
|
||||||
core.info(`Updating comment ${last.id}`);
|
core.info(`Updating comment ${last.id}`);
|
||||||
await github.rest.issues.updateComment({
|
await github.rest.issues.updateComment({
|
||||||
owner, repo,
|
owner, repo,
|
||||||
body,
|
body,
|
||||||
comment_id: last.id,
|
comment_id: last.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
||||||
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const {owner, repo} = context.repo;
|
const {owner, repo} = context.repo;
|
||||||
const run_id = ${{github.event.workflow_run.id}};
|
const run_id = ${{github.event.workflow_run.id}};
|
||||||
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
||||||
if (!pull_requests.length) {
|
if (!pull_requests.length) {
|
||||||
return core.error("This workflow doesn't match any pull requests!");
|
return core.error("This workflow doesn't match any pull requests!");
|
||||||
}
|
}
|
||||||
const artifacts = await github.paginate(
|
const artifacts = await github.paginate(
|
||||||
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
||||||
if (!artifacts.length) {
|
if (!artifacts.length) {
|
||||||
return core.error(`No artifacts found`);
|
return core.error(`No artifacts found`);
|
||||||
}
|
}
|
||||||
let body = `Download the artifacts for this pull request:\n`;
|
let body = `Download the artifacts for this pull request:\n`;
|
||||||
for (const art of artifacts) {
|
for (const art of artifacts) {
|
||||||
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||||
}
|
}
|
||||||
core.info("Review thread message body:", body);
|
core.info("Review thread message body:", body);
|
||||||
for (const pr of pull_requests) {
|
for (const pr of pull_requests) {
|
||||||
await upsertComment(owner, repo, pr.number,
|
await upsertComment(owner, repo, pr.number,
|
||||||
"nightly-link", body);
|
"nightly-link", body);
|
||||||
}
|
}
|
||||||
|
94
.github/workflows/publish-pr.yml
vendored
94
.github/workflows/publish-pr.yml
vendored
@ -1,60 +1,60 @@
|
|||||||
name: Publish (PR)
|
name: Publish (PR)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- development
|
- development
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install --legacy-peer-deps
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
- name: Build releases
|
- name: Build releases
|
||||||
uses: nick-invision/retry@v2.8.2
|
uses: nick-invision/retry@v2.8.2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 30
|
timeout_minutes: 30
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
npm run build
|
npm run build
|
||||||
npm run package:pr
|
npm run package:pr
|
||||||
on_retry_command: npm cache clean --force
|
on_retry_command: npm cache clean --force
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: windows-binaries
|
name: windows-binaries
|
||||||
path: |
|
path: |
|
||||||
release/build/*.exe
|
release/build/*.exe
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: linux-binaries
|
name: linux-binaries
|
||||||
path: |
|
path: |
|
||||||
release/build/*.AppImage
|
release/build/*.AppImage
|
||||||
release/build/*.deb
|
release/build/*.deb
|
||||||
release/build/*.rpm
|
release/build/*.rpm
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: macos-binaries
|
name: macos-binaries
|
||||||
path: |
|
path: |
|
||||||
release/build/*.dmg
|
release/build/*.dmg
|
||||||
|
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@ -3,32 +3,32 @@ name: Test
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Install Node.js and NPM
|
- name: Install Node.js and NPM
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: |
|
run: |
|
||||||
npm install --legacy-peer-deps
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
- name: npm test
|
- name: npm test
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
npm run package
|
npm run lint
|
||||||
npm run lint
|
npm run package
|
||||||
npm exec tsc
|
npm exec tsc
|
||||||
npm test
|
npm test
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
{
|
{
|
||||||
"processors": ["stylelint-processor-styled-components"],
|
"processors": ["stylelint-processor-styled-components"],
|
||||||
"customSyntax": "postcss-scss",
|
"customSyntax": "postcss-scss",
|
||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-standard-scss",
|
"stylelint-config-standard-scss",
|
||||||
"stylelint-config-styled-components",
|
"stylelint-config-styled-components",
|
||||||
"stylelint-config-rational-order"
|
"stylelint-config-rational-order"
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"color-function-notation": ["legacy"],
|
|
||||||
"declaration-empty-line-before": null,
|
|
||||||
"order/properties-order": [],
|
|
||||||
"plugin/rational-order": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"border-in-box-model": false,
|
|
||||||
"empty-line-between-groups": false
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"string-quotes": "single",
|
"rules": {
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"color-function-notation": ["legacy"],
|
||||||
"selector-class-pattern": null,
|
"declaration-empty-line-before": null,
|
||||||
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
"order/properties-order": [],
|
||||||
"selector-type-no-unknown": [
|
"plugin/rational-order": [
|
||||||
true,
|
true,
|
||||||
{ "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }
|
{
|
||||||
],
|
"border-in-box-model": false,
|
||||||
"value-keyword-case": ["lower", { "ignoreKeywords": ["dummyValue"] }],
|
"empty-line-between-groups": false
|
||||||
"declaration-colon-newline-after": null
|
}
|
||||||
}
|
],
|
||||||
|
"string-quotes": "single",
|
||||||
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
|
"selector-class-pattern": null,
|
||||||
|
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
||||||
|
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
||||||
|
"value-keyword-case": ["lower", { "ignoreKeywords": ["dummyValue"] }],
|
||||||
|
"declaration-colon-newline-after": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
.vscode/extensions.json
vendored
12
.vscode/extensions.json
vendored
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"stylelint.vscode-stylelint",
|
"stylelint.vscode-stylelint",
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
54
.vscode/launch.json
vendored
54
.vscode/launch.json
vendored
@ -1,30 +1,28 @@
|
|||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Electron: Main",
|
"name": "Electron: Main",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"protocol": "inspector",
|
"protocol": "inspector",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["run start:main --inspect=5858 --remote-debugging-port=9223"],
|
||||||
"run start:main --inspect=5858 --remote-debugging-port=9223"
|
"preLaunchTask": "Start Webpack Dev"
|
||||||
],
|
},
|
||||||
"preLaunchTask": "Start Webpack Dev"
|
{
|
||||||
},
|
"name": "Electron: Renderer",
|
||||||
{
|
"type": "chrome",
|
||||||
"name": "Electron: Renderer",
|
"request": "attach",
|
||||||
"type": "chrome",
|
"port": 9223,
|
||||||
"request": "attach",
|
"webRoot": "${workspaceFolder}",
|
||||||
"port": 9223,
|
"timeout": 15000
|
||||||
"webRoot": "${workspaceFolder}",
|
}
|
||||||
"timeout": 15000
|
],
|
||||||
}
|
"compounds": [
|
||||||
],
|
{
|
||||||
"compounds": [
|
"name": "Electron: All",
|
||||||
{
|
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||||
"name": "Electron: All",
|
}
|
||||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"label": "Start Webpack Dev",
|
"label": "Start Webpack Dev",
|
||||||
"script": "start:renderer",
|
"script": "start:renderer",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"problemMatcher": {
|
"problemMatcher": {
|
||||||
"owner": "custom",
|
"owner": "custom",
|
||||||
"pattern": {
|
"pattern": {
|
||||||
"regexp": "____________"
|
"regexp": "____________"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"activeOnStart": true,
|
"activeOnStart": true,
|
||||||
"beginsPattern": "Compiling\\.\\.\\.$",
|
"beginsPattern": "Compiling\\.\\.\\.$",
|
||||||
"endsPattern": "(Compiled successfully|Failed to compile)\\.$"
|
"endsPattern": "(Compiled successfully|Failed to compile)\\.$"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
24
assets/assets.d.ts
vendored
24
assets/assets.d.ts
vendored
@ -1,31 +1,31 @@
|
|||||||
type Styles = Record<string, string>;
|
type Styles = Record<string, string>;
|
||||||
|
|
||||||
declare module '*.svg' {
|
declare module '*.svg' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.png' {
|
declare module '*.png' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.jpg' {
|
declare module '*.jpg' {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.scss' {
|
declare module '*.scss' {
|
||||||
const content: Styles;
|
const content: Styles;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.sass' {
|
declare module '*.sass' {
|
||||||
const content: Styles;
|
const content: Styles;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.css' {
|
declare module '*.css' {
|
||||||
const content: Styles;
|
const content: Styles;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
82060
package-lock.json
generated
82060
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
616
package.json
616
package.json
@ -1,321 +1,321 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"productName": "Feishin",
|
|
||||||
"description": "Feishin music server",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"scripts": {
|
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
|
||||||
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
|
||||||
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
|
||||||
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
|
||||||
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
|
||||||
"lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
|
||||||
"lint:styles": "npx stylelint **/*.tsx",
|
|
||||||
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
|
||||||
"package:pr": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
|
||||||
"package:dev": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
|
||||||
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
|
||||||
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
|
||||||
"start:main": "cross-env NODE_ENV=development electron -r ts-node/register/transpile-only ./src/main/main.ts",
|
|
||||||
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
|
||||||
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
|
||||||
"start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.web.ts",
|
|
||||||
"test": "jest",
|
|
||||||
"prepare": "husky install",
|
|
||||||
"i18next": "i18next -c src/renderer/i18n/i18next-parser.config.js",
|
|
||||||
"prod:buildserver": "pwsh -c \"./scripts/server-build.ps1\"",
|
|
||||||
"prod:publishserver": "pwsh -c \"./scripts/server-publish.ps1\""
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{js,jsx,ts,tsx}": [
|
|
||||||
"cross-env NODE_ENV=development eslint --cache"
|
|
||||||
],
|
|
||||||
"*.json,.{eslintrc,prettierrc}": [
|
|
||||||
"prettier --ignore-path .eslintignore --parser json --write"
|
|
||||||
],
|
|
||||||
"*.{css,scss}": [
|
|
||||||
"prettier --ignore-path .eslintignore --single-quote --write"
|
|
||||||
],
|
|
||||||
"*.{html,md,yml}": [
|
|
||||||
"prettier --ignore-path .eslintignore --single-quote --write"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"productName": "Feishin",
|
"productName": "Feishin",
|
||||||
"appId": "org.jeffvli.feishin",
|
"description": "Feishin music server",
|
||||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
"version": "0.2.0",
|
||||||
"asar": true,
|
"scripts": {
|
||||||
"asarUnpack": "**\\*.{node,dll}",
|
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||||
"files": [
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
"dist",
|
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
||||||
"node_modules",
|
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
||||||
"package.json"
|
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
],
|
"lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
"afterSign": ".erb/scripts/notarize.js",
|
"lint:styles": "npx stylelint **/*.tsx",
|
||||||
"electronVersion": "22.3.1",
|
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
||||||
"mac": {
|
"package:pr": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
||||||
"target": {
|
"package:dev": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
||||||
"target": "default",
|
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
||||||
"arch": [
|
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
||||||
"arm64",
|
"start:main": "cross-env NODE_ENV=development electron -r ts-node/register/transpile-only ./src/main/main.ts",
|
||||||
"x64"
|
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||||
|
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||||
|
"start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.web.ts",
|
||||||
|
"test": "jest",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"i18next": "i18next -c src/renderer/i18n/i18next-parser.config.js",
|
||||||
|
"prod:buildserver": "pwsh -c \"./scripts/server-build.ps1\"",
|
||||||
|
"prod:publishserver": "pwsh -c \"./scripts/server-publish.ps1\""
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx}": [
|
||||||
|
"cross-env NODE_ENV=development eslint --cache"
|
||||||
|
],
|
||||||
|
"*.json,.{eslintrc,prettierrc}": [
|
||||||
|
"prettier --ignore-path .eslintignore --parser json --write"
|
||||||
|
],
|
||||||
|
"*.{css,scss}": [
|
||||||
|
"prettier --ignore-path .eslintignore --single-quote --write"
|
||||||
|
],
|
||||||
|
"*.{html,md,yml}": [
|
||||||
|
"prettier --ignore-path .eslintignore --single-quote --write"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"type": "distribution",
|
|
||||||
"hardenedRuntime": true,
|
|
||||||
"entitlements": "assets/entitlements.mac.plist",
|
|
||||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
|
||||||
"gatekeeperAssess": false
|
|
||||||
},
|
},
|
||||||
"dmg": {
|
"build": {
|
||||||
"contents": [
|
"productName": "Feishin",
|
||||||
{
|
"appId": "org.jeffvli.feishin",
|
||||||
"x": 130,
|
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
||||||
"y": 220
|
"asar": true,
|
||||||
|
"asarUnpack": "**\\*.{node,dll}",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"afterSign": ".erb/scripts/notarize.js",
|
||||||
|
"electronVersion": "22.3.1",
|
||||||
|
"mac": {
|
||||||
|
"target": {
|
||||||
|
"target": "default",
|
||||||
|
"arch": [
|
||||||
|
"arm64",
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "distribution",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"entitlements": "assets/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||||
|
"gatekeeperAssess": false
|
||||||
},
|
},
|
||||||
{
|
"dmg": {
|
||||||
"x": 410,
|
"contents": [
|
||||||
"y": 220,
|
{
|
||||||
"type": "link",
|
"x": 130,
|
||||||
"path": "/Applications"
|
"y": 220
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 410,
|
||||||
|
"y": 220,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/Applications"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis",
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"tar.xz"
|
||||||
|
],
|
||||||
|
"icon": "assets/icons/placeholder.png",
|
||||||
|
"category": "Development"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"app": "release/app",
|
||||||
|
"buildResources": "assets",
|
||||||
|
"output": "release/build"
|
||||||
|
},
|
||||||
|
"extraResources": [
|
||||||
|
"./assets/**"
|
||||||
|
],
|
||||||
|
"publish": {
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "jeffvli",
|
||||||
|
"repo": "feishin"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"win": {
|
"repository": {
|
||||||
"target": [
|
"type": "git",
|
||||||
"nsis",
|
"url": "git+https://github.com/jeffvli/feishin.git"
|
||||||
"zip"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"linux": {
|
"author": {
|
||||||
"target": [
|
"name": "jeffvli",
|
||||||
"AppImage",
|
"url": "https://github.com/jeffvli/"
|
||||||
"tar.xz"
|
|
||||||
],
|
|
||||||
"icon": "assets/icons/placeholder.png",
|
|
||||||
"category": "Development"
|
|
||||||
},
|
},
|
||||||
"directories": {
|
"contributors": [],
|
||||||
"app": "release/app",
|
"license": "GPL-3.0",
|
||||||
"buildResources": "assets",
|
"bugs": {
|
||||||
"output": "release/build"
|
"url": "https://github.com/jeffvli/feishin/issues"
|
||||||
},
|
},
|
||||||
"extraResources": [
|
"keywords": [
|
||||||
"./assets/**"
|
"subsonic",
|
||||||
|
"navidrome",
|
||||||
|
"airsonic",
|
||||||
|
"jellyfin",
|
||||||
|
"react",
|
||||||
|
"electron"
|
||||||
],
|
],
|
||||||
"publish": {
|
"homepage": "https://github.com/jeffvli/feishin",
|
||||||
"provider": "github",
|
"jest": {
|
||||||
"owner": "jeffvli",
|
"testURL": "http://localhost/",
|
||||||
"repo": "feishin"
|
"testEnvironment": "jsdom",
|
||||||
|
"transform": {
|
||||||
|
"\\.(ts|tsx|js|jsx)$": "ts-jest"
|
||||||
|
},
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
|
||||||
|
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"jsx",
|
||||||
|
"ts",
|
||||||
|
"tsx",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"moduleDirectories": [
|
||||||
|
"node_modules",
|
||||||
|
"release/app/node_modules"
|
||||||
|
],
|
||||||
|
"testPathIgnorePatterns": [
|
||||||
|
"release/app/dist"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"./.erb/scripts/check-build-exists.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron/rebuild": "^3.2.10",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
||||||
|
"@stylelint/postcss-css-in-js": "^0.38.0",
|
||||||
|
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
||||||
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
|
"@testing-library/react": "^13.0.0",
|
||||||
|
"@types/electron-localshortcut": "^3.1.0",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"@types/lodash": "^4.14.188",
|
||||||
|
"@types/md5": "^2.3.2",
|
||||||
|
"@types/node": "^17.0.23",
|
||||||
|
"@types/react": "^18.0.25",
|
||||||
|
"@types/react-dom": "^18.0.8",
|
||||||
|
"@types/react-test-renderer": "^17.0.1",
|
||||||
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
|
"@types/react-window": "^1.8.5",
|
||||||
|
"@types/react-window-infinite-loader": "^1.0.6",
|
||||||
|
"@types/styled-components": "^5.1.26",
|
||||||
|
"@types/terser-webpack-plugin": "^5.0.4",
|
||||||
|
"@types/webpack-bundle-analyzer": "^4.4.1",
|
||||||
|
"@types/webpack-env": "^1.16.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||||
|
"@typescript-eslint/parser": "^5.47.0",
|
||||||
|
"browserslist-config-erb": "^0.0.3",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"concurrently": "^7.1.0",
|
||||||
|
"core-js": "^3.21.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^6.7.1",
|
||||||
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
|
"detect-port": "^1.3.0",
|
||||||
|
"electron": "^22.3.1",
|
||||||
|
"electron-builder": "^24.0.0-alpha.13",
|
||||||
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electron-notarize": "^1.2.1",
|
||||||
|
"electronmon": "^2.0.2",
|
||||||
|
"eslint": "^8.30.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-erb": "^4.0.3",
|
||||||
|
"eslint-import-resolver-typescript": "^2.7.1",
|
||||||
|
"eslint-import-resolver-webpack": "^0.13.2",
|
||||||
|
"eslint-plugin-compat": "^4.0.2",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-jest": "^26.1.3",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
|
"eslint-plugin-react": "^7.29.4",
|
||||||
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
||||||
|
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"husky": "^7.0.4",
|
||||||
|
"i18next-parser": "^6.3.0",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"lint-staged": "^12.3.7",
|
||||||
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
|
"postcss-scss": "^4.0.4",
|
||||||
|
"postcss-syntax": "^0.36.2",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"react-refresh": "^0.12.0",
|
||||||
|
"react-refresh-typescript": "^2.0.4",
|
||||||
|
"react-test-renderer": "^18.0.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sass": "^1.49.11",
|
||||||
|
"sass-loader": "^12.6.0",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"stylelint": "^14.9.1",
|
||||||
|
"stylelint-config-rational-order": "^0.1.2",
|
||||||
|
"stylelint-config-standard-scss": "^4.0.0",
|
||||||
|
"stylelint-config-styled-components": "^0.1.1",
|
||||||
|
"stylelint-order": "^5.0.0",
|
||||||
|
"stylelint-processor-styled-components": "^1.10.0",
|
||||||
|
"terser-webpack-plugin": "^5.3.1",
|
||||||
|
"ts-jest": "^27.1.4",
|
||||||
|
"ts-loader": "^9.2.8",
|
||||||
|
"ts-node": "^10.7.0",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
|
"typescript": "^4.8.4",
|
||||||
|
"typescript-plugin-styled-components": "^2.0.0",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.71.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "^4.8.0",
|
||||||
|
"webpack-merge": "^5.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ag-grid-community/client-side-row-model": "^28.2.1",
|
||||||
|
"@ag-grid-community/core": "^28.2.1",
|
||||||
|
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
||||||
|
"@ag-grid-community/react": "^28.2.1",
|
||||||
|
"@ag-grid-community/styles": "^28.2.1",
|
||||||
|
"@emotion/react": "^11.10.4",
|
||||||
|
"@mantine/core": "^6.0.13",
|
||||||
|
"@mantine/dates": "^6.0.13",
|
||||||
|
"@mantine/form": "^6.0.13",
|
||||||
|
"@mantine/hooks": "^6.0.13",
|
||||||
|
"@mantine/modals": "^6.0.13",
|
||||||
|
"@mantine/notifications": "^6.0.13",
|
||||||
|
"@mantine/utils": "^6.0.13",
|
||||||
|
"@tanstack/react-query": "^4.29.5",
|
||||||
|
"@tanstack/react-query-devtools": "^4.29.6",
|
||||||
|
"@ts-rest/core": "^3.23.0",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"cmdk": "^0.2.0",
|
||||||
|
"dayjs": "^1.11.6",
|
||||||
|
"electron-debug": "^3.2.0",
|
||||||
|
"electron-localshortcut": "^3.2.1",
|
||||||
|
"electron-log": "^4.4.6",
|
||||||
|
"electron-store": "^8.1.0",
|
||||||
|
"electron-updater": "^4.6.5",
|
||||||
|
"fast-average-color": "^9.3.0",
|
||||||
|
"format-duration": "^2.0.0",
|
||||||
|
"framer-motion": "^9.1.7",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
|
"history": "^5.3.0",
|
||||||
|
"i18next": "^21.6.16",
|
||||||
|
"immer": "^9.0.21",
|
||||||
|
"is-electron": "^2.2.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"md5": "^2.3.0",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"nanoid": "^3.3.3",
|
||||||
|
"net": "^1.0.2",
|
||||||
|
"node-mpv": "github:jeffvli/Node-MPV",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-error-boundary": "^3.1.4",
|
||||||
|
"react-i18next": "^11.16.7",
|
||||||
|
"react-icons": "^4.8.0",
|
||||||
|
"react-player": "^2.11.0",
|
||||||
|
"react-router": "^6.5.0",
|
||||||
|
"react-router-dom": "^6.5.0",
|
||||||
|
"react-simple-img": "^3.0.0",
|
||||||
|
"react-virtualized-auto-sizer": "^1.0.17",
|
||||||
|
"react-window": "^1.8.9",
|
||||||
|
"react-window-infinite-loader": "^1.0.9",
|
||||||
|
"styled-components": "^5.3.11",
|
||||||
|
"swiper": "^9.3.1",
|
||||||
|
"zod": "^3.21.4",
|
||||||
|
"zustand": "^4.3.8"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"styled-components": "^5"
|
||||||
|
},
|
||||||
|
"devEngines": {
|
||||||
|
"node": ">=14.x",
|
||||||
|
"npm": ">=7.x"
|
||||||
|
},
|
||||||
|
"browserslist": [],
|
||||||
|
"electronmon": {
|
||||||
|
"patterns": [
|
||||||
|
"!server",
|
||||||
|
"!src/renderer"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/jeffvli/feishin.git"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "jeffvli",
|
|
||||||
"url": "https://github.com/jeffvli/"
|
|
||||||
},
|
|
||||||
"contributors": [],
|
|
||||||
"license": "GPL-3.0",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/jeffvli/feishin/issues"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"subsonic",
|
|
||||||
"navidrome",
|
|
||||||
"airsonic",
|
|
||||||
"jellyfin",
|
|
||||||
"react",
|
|
||||||
"electron"
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/jeffvli/feishin",
|
|
||||||
"jest": {
|
|
||||||
"testURL": "http://localhost/",
|
|
||||||
"testEnvironment": "jsdom",
|
|
||||||
"transform": {
|
|
||||||
"\\.(ts|tsx|js|jsx)$": "ts-jest"
|
|
||||||
},
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
|
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
|
||||||
},
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"jsx",
|
|
||||||
"ts",
|
|
||||||
"tsx",
|
|
||||||
"json"
|
|
||||||
],
|
|
||||||
"moduleDirectories": [
|
|
||||||
"node_modules",
|
|
||||||
"release/app/node_modules"
|
|
||||||
],
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
"release/app/dist"
|
|
||||||
],
|
|
||||||
"setupFiles": [
|
|
||||||
"./.erb/scripts/check-build-exists.ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@electron/rebuild": "^3.2.10",
|
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
|
||||||
"@stylelint/postcss-css-in-js": "^0.38.0",
|
|
||||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
|
||||||
"@testing-library/react": "^13.0.0",
|
|
||||||
"@types/electron-localshortcut": "^3.1.0",
|
|
||||||
"@types/jest": "^27.4.1",
|
|
||||||
"@types/lodash": "^4.14.188",
|
|
||||||
"@types/md5": "^2.3.2",
|
|
||||||
"@types/node": "^17.0.23",
|
|
||||||
"@types/react": "^18.0.25",
|
|
||||||
"@types/react-dom": "^18.0.8",
|
|
||||||
"@types/react-test-renderer": "^17.0.1",
|
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
|
||||||
"@types/react-window": "^1.8.5",
|
|
||||||
"@types/react-window-infinite-loader": "^1.0.6",
|
|
||||||
"@types/styled-components": "^5.1.26",
|
|
||||||
"@types/terser-webpack-plugin": "^5.0.4",
|
|
||||||
"@types/webpack-bundle-analyzer": "^4.4.1",
|
|
||||||
"@types/webpack-env": "^1.16.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
|
||||||
"@typescript-eslint/parser": "^5.47.0",
|
|
||||||
"browserslist-config-erb": "^0.0.3",
|
|
||||||
"chalk": "^4.1.2",
|
|
||||||
"concurrently": "^7.1.0",
|
|
||||||
"core-js": "^3.21.1",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"css-loader": "^6.7.1",
|
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
|
||||||
"detect-port": "^1.3.0",
|
|
||||||
"electron": "^22.3.1",
|
|
||||||
"electron-builder": "^24.0.0-alpha.13",
|
|
||||||
"electron-devtools-installer": "^3.2.0",
|
|
||||||
"electron-notarize": "^1.2.1",
|
|
||||||
"electronmon": "^2.0.2",
|
|
||||||
"eslint": "^8.30.0",
|
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
|
||||||
"eslint-config-erb": "^4.0.3",
|
|
||||||
"eslint-import-resolver-typescript": "^2.7.1",
|
|
||||||
"eslint-import-resolver-webpack": "^0.13.2",
|
|
||||||
"eslint-plugin-compat": "^4.0.2",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"eslint-plugin-jest": "^26.1.3",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
|
||||||
"eslint-plugin-react": "^7.29.4",
|
|
||||||
"eslint-plugin-react-hooks": "^4.4.0",
|
|
||||||
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
|
||||||
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"html-webpack-plugin": "^5.5.0",
|
|
||||||
"husky": "^7.0.4",
|
|
||||||
"i18next-parser": "^6.3.0",
|
|
||||||
"identity-obj-proxy": "^3.0.0",
|
|
||||||
"jest": "^27.5.1",
|
|
||||||
"lint-staged": "^12.3.7",
|
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
|
||||||
"postcss-scss": "^4.0.4",
|
|
||||||
"postcss-syntax": "^0.36.2",
|
|
||||||
"prettier": "^2.6.2",
|
|
||||||
"react-refresh": "^0.12.0",
|
|
||||||
"react-refresh-typescript": "^2.0.4",
|
|
||||||
"react-test-renderer": "^18.0.0",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"sass": "^1.49.11",
|
|
||||||
"sass-loader": "^12.6.0",
|
|
||||||
"style-loader": "^3.3.1",
|
|
||||||
"stylelint": "^14.9.1",
|
|
||||||
"stylelint-config-rational-order": "^0.1.2",
|
|
||||||
"stylelint-config-standard-scss": "^4.0.0",
|
|
||||||
"stylelint-config-styled-components": "^0.1.1",
|
|
||||||
"stylelint-order": "^5.0.0",
|
|
||||||
"stylelint-processor-styled-components": "^1.10.0",
|
|
||||||
"terser-webpack-plugin": "^5.3.1",
|
|
||||||
"ts-jest": "^27.1.4",
|
|
||||||
"ts-loader": "^9.2.8",
|
|
||||||
"ts-node": "^10.7.0",
|
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
|
||||||
"typescript": "^4.8.4",
|
|
||||||
"typescript-plugin-styled-components": "^2.0.0",
|
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"webpack": "^5.71.0",
|
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
|
||||||
"webpack-cli": "^4.9.2",
|
|
||||||
"webpack-dev-server": "^4.8.0",
|
|
||||||
"webpack-merge": "^5.8.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@ag-grid-community/client-side-row-model": "^28.2.1",
|
|
||||||
"@ag-grid-community/core": "^28.2.1",
|
|
||||||
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
|
||||||
"@ag-grid-community/react": "^28.2.1",
|
|
||||||
"@ag-grid-community/styles": "^28.2.1",
|
|
||||||
"@emotion/react": "^11.10.4",
|
|
||||||
"@mantine/core": "^6.0.13",
|
|
||||||
"@mantine/dates": "^6.0.13",
|
|
||||||
"@mantine/form": "^6.0.13",
|
|
||||||
"@mantine/hooks": "^6.0.13",
|
|
||||||
"@mantine/modals": "^6.0.13",
|
|
||||||
"@mantine/notifications": "^6.0.13",
|
|
||||||
"@mantine/utils": "^6.0.13",
|
|
||||||
"@tanstack/react-query": "^4.29.5",
|
|
||||||
"@tanstack/react-query-devtools": "^4.29.6",
|
|
||||||
"@ts-rest/core": "^3.23.0",
|
|
||||||
"axios": "^1.4.0",
|
|
||||||
"cmdk": "^0.2.0",
|
|
||||||
"dayjs": "^1.11.6",
|
|
||||||
"electron-debug": "^3.2.0",
|
|
||||||
"electron-localshortcut": "^3.2.1",
|
|
||||||
"electron-log": "^4.4.6",
|
|
||||||
"electron-store": "^8.1.0",
|
|
||||||
"electron-updater": "^4.6.5",
|
|
||||||
"fast-average-color": "^9.3.0",
|
|
||||||
"format-duration": "^2.0.0",
|
|
||||||
"framer-motion": "^9.1.7",
|
|
||||||
"fuse.js": "^6.6.2",
|
|
||||||
"history": "^5.3.0",
|
|
||||||
"i18next": "^21.6.16",
|
|
||||||
"immer": "^9.0.21",
|
|
||||||
"is-electron": "^2.2.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"md5": "^2.3.0",
|
|
||||||
"memoize-one": "^6.0.0",
|
|
||||||
"nanoid": "^3.3.3",
|
|
||||||
"net": "^1.0.2",
|
|
||||||
"node-mpv": "github:jeffvli/Node-MPV",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-error-boundary": "^3.1.4",
|
|
||||||
"react-i18next": "^11.16.7",
|
|
||||||
"react-icons": "^4.8.0",
|
|
||||||
"react-player": "^2.11.0",
|
|
||||||
"react-router": "^6.5.0",
|
|
||||||
"react-router-dom": "^6.5.0",
|
|
||||||
"react-simple-img": "^3.0.0",
|
|
||||||
"react-virtualized-auto-sizer": "^1.0.17",
|
|
||||||
"react-window": "^1.8.9",
|
|
||||||
"react-window-infinite-loader": "^1.0.9",
|
|
||||||
"styled-components": "^5.3.11",
|
|
||||||
"swiper": "^9.3.1",
|
|
||||||
"zod": "^3.21.4",
|
|
||||||
"zustand": "^4.3.8"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"styled-components": "^5"
|
|
||||||
},
|
|
||||||
"devEngines": {
|
|
||||||
"node": ">=14.x",
|
|
||||||
"npm": ">=7.x"
|
|
||||||
},
|
|
||||||
"browserslist": [],
|
|
||||||
"electronmon": {
|
|
||||||
"patterns": [
|
|
||||||
"!server",
|
|
||||||
"!src/renderer"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/react';
|
|||||||
import { App } from '../renderer/app';
|
import { App } from '../renderer/app';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
it('should render', () => {
|
it('should render', () => {
|
||||||
expect(render(<App />)).toBeTruthy();
|
expect(render(<App />)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
import {
|
import {
|
||||||
LyricSource,
|
LyricSource,
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
} from '../../../../renderer/api/types';
|
} from '../../../../renderer/api/types';
|
||||||
import { orderSearchResults } from './shared';
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
@ -13,197 +13,197 @@ const SEARCH_URL = 'https://genius.com/api/search/song';
|
|||||||
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/genius.ts
|
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/genius.ts
|
||||||
|
|
||||||
export interface GeniusResponse {
|
export interface GeniusResponse {
|
||||||
meta: Meta;
|
meta: Meta;
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Meta {
|
export interface Meta {
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Response {
|
export interface Response {
|
||||||
next_page: number;
|
next_page: number;
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Section {
|
export interface Section {
|
||||||
hits: Hit[];
|
hits: Hit[];
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hit {
|
export interface Hit {
|
||||||
highlights: any[];
|
highlights: any[];
|
||||||
index: string;
|
index: string;
|
||||||
result: Result;
|
result: Result;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Result {
|
export interface Result {
|
||||||
_type: string;
|
_type: string;
|
||||||
annotation_count: number;
|
annotation_count: number;
|
||||||
api_path: string;
|
api_path: string;
|
||||||
artist_names: string;
|
artist_names: string;
|
||||||
featured_artists: any[];
|
featured_artists: any[];
|
||||||
full_title: string;
|
full_title: string;
|
||||||
header_image_thumbnail_url: string;
|
header_image_thumbnail_url: string;
|
||||||
header_image_url: string;
|
header_image_url: string;
|
||||||
id: number;
|
id: number;
|
||||||
instrumental: boolean;
|
instrumental: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
lyrics_owner_id: number;
|
lyrics_owner_id: number;
|
||||||
lyrics_state: string;
|
lyrics_state: string;
|
||||||
lyrics_updated_at: number;
|
lyrics_updated_at: number;
|
||||||
path: string;
|
path: string;
|
||||||
primary_artist: PrimaryArtist;
|
primary_artist: PrimaryArtist;
|
||||||
pyongs_count: null;
|
pyongs_count: null;
|
||||||
relationships_index_url: string;
|
relationships_index_url: string;
|
||||||
release_date_components: ReleaseDateComponents;
|
release_date_components: ReleaseDateComponents;
|
||||||
release_date_for_display: string;
|
release_date_for_display: string;
|
||||||
release_date_with_abbreviated_month_for_display: string;
|
release_date_with_abbreviated_month_for_display: string;
|
||||||
song_art_image_thumbnail_url: string;
|
song_art_image_thumbnail_url: string;
|
||||||
song_art_image_url: string;
|
song_art_image_url: string;
|
||||||
stats: Stats;
|
stats: Stats;
|
||||||
title: string;
|
title: string;
|
||||||
title_with_featured: string;
|
title_with_featured: string;
|
||||||
updated_by_human_at: number;
|
updated_by_human_at: number;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrimaryArtist {
|
export interface PrimaryArtist {
|
||||||
_type: string;
|
_type: string;
|
||||||
api_path: string;
|
api_path: string;
|
||||||
header_image_url: string;
|
header_image_url: string;
|
||||||
id: number;
|
id: number;
|
||||||
image_url: string;
|
image_url: string;
|
||||||
index_character: string;
|
index_character: string;
|
||||||
is_meme_verified: boolean;
|
is_meme_verified: boolean;
|
||||||
is_verified: boolean;
|
is_verified: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReleaseDateComponents {
|
export interface ReleaseDateComponents {
|
||||||
day: number;
|
day: number;
|
||||||
month: number;
|
month: number;
|
||||||
year: number;
|
year: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Stats {
|
export interface Stats {
|
||||||
hot: boolean;
|
hot: boolean;
|
||||||
unreviewed_annotations: number;
|
unreviewed_annotations: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function getSearchResults(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||||
let result: AxiosResponse<GeniusResponse>;
|
let result: AxiosResponse<GeniusResponse>;
|
||||||
|
|
||||||
const searchQuery = [params.artist, params.name].join(' ');
|
const searchQuery = [params.artist, params.name].join(' ');
|
||||||
|
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await axios.get(SEARCH_URL, {
|
result = await axios.get(SEARCH_URL, {
|
||||||
params: {
|
params: {
|
||||||
per_page: '5',
|
per_page: '5',
|
||||||
q: searchQuery,
|
q: searchQuery,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Genius search request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawSongsResult = result.data.response?.sections?.[0]?.hits?.map((hit) => hit.result);
|
||||||
|
|
||||||
|
if (!rawSongsResult) return null;
|
||||||
|
|
||||||
|
const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map((song) => {
|
||||||
|
return {
|
||||||
|
artist: song.artist_names,
|
||||||
|
id: song.url,
|
||||||
|
name: song.full_title,
|
||||||
|
source: LyricSource.GENIUS,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
console.error('Genius search request got an error!', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawSongsResult = result.data.response?.sections?.[0]?.hits?.map((hit) => hit.result);
|
return orderSearchResults({ params, results: songResults });
|
||||||
|
|
||||||
if (!rawSongsResult) return null;
|
|
||||||
|
|
||||||
const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map((song) => {
|
|
||||||
return {
|
|
||||||
artist: song.artist_names,
|
|
||||||
id: song.url,
|
|
||||||
name: song.full_title,
|
|
||||||
source: LyricSource.GENIUS,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return orderSearchResults({ params, results: songResults });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSongId(
|
async function getSongId(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
|
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
|
||||||
let result: AxiosResponse<GeniusResponse>;
|
let result: AxiosResponse<GeniusResponse>;
|
||||||
try {
|
try {
|
||||||
result = await axios.get(SEARCH_URL, {
|
result = await axios.get(SEARCH_URL, {
|
||||||
params: {
|
params: {
|
||||||
per_page: '1',
|
per_page: '1',
|
||||||
q: `${params.artist} ${params.name}`,
|
q: `${params.artist} ${params.name}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Genius search request got an error!', e);
|
console.error('Genius search request got an error!', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hit = result.data.response?.sections?.[0]?.hits?.[0]?.result;
|
const hit = result.data.response?.sections?.[0]?.hits?.[0]?.result;
|
||||||
|
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artist: hit.artist_names,
|
artist: hit.artist_names,
|
||||||
id: hit.url,
|
id: hit.url,
|
||||||
name: hit.full_title,
|
name: hit.full_title,
|
||||||
source: LyricSource.GENIUS,
|
source: LyricSource.GENIUS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLyricsBySongId(url: string): Promise<string | null> {
|
export async function getLyricsBySongId(url: string): Promise<string | null> {
|
||||||
let result: AxiosResponse<string, any>;
|
let result: AxiosResponse<string, any>;
|
||||||
try {
|
try {
|
||||||
result = await axios.get<string>(url, { responseType: 'text' });
|
result = await axios.get<string>(url, { responseType: 'text' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Genius lyrics request got an error!', e);
|
console.error('Genius lyrics request got an error!', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $ = load(result.data.split('<br/>').join('\n'));
|
const $ = load(result.data.split('<br/>').join('\n'));
|
||||||
const lyricsDiv = $('div.lyrics');
|
const lyricsDiv = $('div.lyrics');
|
||||||
|
|
||||||
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
|
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
|
||||||
|
|
||||||
const lyricSections = $('div[class^=Lyrics__Container]')
|
const lyricSections = $('div[class^=Lyrics__Container]')
|
||||||
.map((_, e) => $(e).text())
|
.map((_, e) => $(e).text())
|
||||||
.toArray()
|
.toArray()
|
||||||
.join('\n');
|
.join('\n');
|
||||||
return lyricSections;
|
return lyricSections;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricResponse | null> {
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
const response = await getSongId(params);
|
const response = await getSongId(params);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error('Could not find the song on Genius!');
|
console.error('Could not find the song on Genius!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lyrics = await getLyricsBySongId(response.id);
|
const lyrics = await getLyricsBySongId(response.id);
|
||||||
if (!lyrics) {
|
if (!lyrics) {
|
||||||
console.error('Could not get lyrics on Genius!');
|
console.error('Could not get lyrics on Genius!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artist: response.artist,
|
artist: response.artist,
|
||||||
id: response.id,
|
id: response.id,
|
||||||
lyrics,
|
lyrics,
|
||||||
name: response.name,
|
name: response.name,
|
||||||
source: LyricSource.GENIUS,
|
source: LyricSource.GENIUS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,53 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
QueueSong,
|
QueueSong,
|
||||||
LyricGetQuery,
|
LyricGetQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
} from '../../../../renderer/api/types';
|
} from '../../../../renderer/api/types';
|
||||||
import { store } from '../settings/index';
|
import { store } from '../settings/index';
|
||||||
import {
|
import {
|
||||||
query as queryGenius,
|
query as queryGenius,
|
||||||
getSearchResults as searchGenius,
|
getSearchResults as searchGenius,
|
||||||
getLyricsBySongId as getGenius,
|
getLyricsBySongId as getGenius,
|
||||||
} from './genius';
|
} from './genius';
|
||||||
import {
|
import {
|
||||||
query as queryLrclib,
|
query as queryLrclib,
|
||||||
getSearchResults as searchLrcLib,
|
getSearchResults as searchLrcLib,
|
||||||
getLyricsBySongId as getLrcLib,
|
getLyricsBySongId as getLrcLib,
|
||||||
} from './lrclib';
|
} from './lrclib';
|
||||||
import {
|
import {
|
||||||
query as queryNetease,
|
query as queryNetease,
|
||||||
getSearchResults as searchNetease,
|
getSearchResults as searchNetease,
|
||||||
getLyricsBySongId as getNetease,
|
getLyricsBySongId as getNetease,
|
||||||
} from './netease';
|
} from './netease';
|
||||||
|
|
||||||
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
|
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
|
||||||
type SearchFetcher = (
|
type SearchFetcher = (
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
) => Promise<InternetProviderLyricSearchResponse[] | null>;
|
) => Promise<InternetProviderLyricSearchResponse[] | null>;
|
||||||
type GetFetcher = (id: string) => Promise<string | null>;
|
type GetFetcher = (id: string) => Promise<string | null>;
|
||||||
|
|
||||||
type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
|
type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
|
||||||
|
|
||||||
const FETCHERS: Record<LyricSource, SongFetcher> = {
|
const FETCHERS: Record<LyricSource, SongFetcher> = {
|
||||||
[LyricSource.GENIUS]: queryGenius,
|
[LyricSource.GENIUS]: queryGenius,
|
||||||
[LyricSource.LRCLIB]: queryLrclib,
|
[LyricSource.LRCLIB]: queryLrclib,
|
||||||
[LyricSource.NETEASE]: queryNetease,
|
[LyricSource.NETEASE]: queryNetease,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
|
const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
|
||||||
[LyricSource.GENIUS]: searchGenius,
|
[LyricSource.GENIUS]: searchGenius,
|
||||||
[LyricSource.LRCLIB]: searchLrcLib,
|
[LyricSource.LRCLIB]: searchLrcLib,
|
||||||
[LyricSource.NETEASE]: searchNetease,
|
[LyricSource.NETEASE]: searchNetease,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GET_FETCHERS: Record<LyricSource, GetFetcher> = {
|
const GET_FETCHERS: Record<LyricSource, GetFetcher> = {
|
||||||
[LyricSource.GENIUS]: getGenius,
|
[LyricSource.GENIUS]: getGenius,
|
||||||
[LyricSource.LRCLIB]: getLrcLib,
|
[LyricSource.LRCLIB]: getLrcLib,
|
||||||
[LyricSource.NETEASE]: getNetease,
|
[LyricSource.NETEASE]: getNetease,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_CACHED_ITEMS = 10;
|
const MAX_CACHED_ITEMS = 10;
|
||||||
@ -55,95 +55,95 @@ const MAX_CACHED_ITEMS = 10;
|
|||||||
const lyricCache = new Map<string, CachedLyrics>();
|
const lyricCache = new Map<string, CachedLyrics>();
|
||||||
|
|
||||||
const getRemoteLyrics = async (song: QueueSong) => {
|
const getRemoteLyrics = async (song: QueueSong) => {
|
||||||
const sources = store.get('lyrics', []) as LyricSource[];
|
const sources = store.get('lyrics', []) as LyricSource[];
|
||||||
|
|
||||||
const cached = lyricCache.get(song.id);
|
const cached = lyricCache.get(song.id);
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
for (const source of sources) {
|
||||||
|
const data = cached[source];
|
||||||
|
if (data) return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lyricsFromSource = null;
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
const data = cached[source];
|
const params = {
|
||||||
if (data) return data;
|
album: song.album || song.name,
|
||||||
|
artist: song.artistName,
|
||||||
|
duration: song.duration,
|
||||||
|
name: song.name,
|
||||||
|
};
|
||||||
|
const response = await FETCHERS[source](params);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
const newResult = cached
|
||||||
|
? {
|
||||||
|
...cached,
|
||||||
|
[source]: response,
|
||||||
|
}
|
||||||
|
: ({ [source]: response } as CachedLyrics);
|
||||||
|
|
||||||
|
if (lyricCache.size === MAX_CACHED_ITEMS && cached === undefined) {
|
||||||
|
const toRemove = lyricCache.keys().next().value;
|
||||||
|
lyricCache.delete(toRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricCache.set(song.id, newResult);
|
||||||
|
|
||||||
|
lyricsFromSource = response;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let lyricsFromSource = null;
|
return lyricsFromSource;
|
||||||
|
|
||||||
for (const source of sources) {
|
|
||||||
const params = {
|
|
||||||
album: song.album || song.name,
|
|
||||||
artist: song.artistName,
|
|
||||||
duration: song.duration,
|
|
||||||
name: song.name,
|
|
||||||
};
|
|
||||||
const response = await FETCHERS[source](params);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
const newResult = cached
|
|
||||||
? {
|
|
||||||
...cached,
|
|
||||||
[source]: response,
|
|
||||||
}
|
|
||||||
: ({ [source]: response } as CachedLyrics);
|
|
||||||
|
|
||||||
if (lyricCache.size === MAX_CACHED_ITEMS && cached === undefined) {
|
|
||||||
const toRemove = lyricCache.keys().next().value;
|
|
||||||
lyricCache.delete(toRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
lyricCache.set(song.id, newResult);
|
|
||||||
|
|
||||||
lyricsFromSource = response;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lyricsFromSource;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchRemoteLyrics = async (params: LyricSearchQuery) => {
|
const searchRemoteLyrics = async (params: LyricSearchQuery) => {
|
||||||
const sources = store.get('lyrics', []) as LyricSource[];
|
const sources = store.get('lyrics', []) as LyricSource[];
|
||||||
|
|
||||||
const results: Record<LyricSource, InternetProviderLyricSearchResponse[]> = {
|
const results: Record<LyricSource, InternetProviderLyricSearchResponse[]> = {
|
||||||
[LyricSource.GENIUS]: [],
|
[LyricSource.GENIUS]: [],
|
||||||
[LyricSource.LRCLIB]: [],
|
[LyricSource.LRCLIB]: [],
|
||||||
[LyricSource.NETEASE]: [],
|
[LyricSource.NETEASE]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
const response = await SEARCH_FETCHERS[source](params);
|
const response = await SEARCH_FETCHERS[source](params);
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
response.forEach((result) => {
|
response.forEach((result) => {
|
||||||
results[source].push(result);
|
results[source].push(result);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null> => {
|
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null> => {
|
||||||
const { remoteSongId, remoteSource } = params;
|
const { remoteSongId, remoteSource } = params;
|
||||||
const response = await GET_FETCHERS[remoteSource](remoteSongId);
|
const response = await GET_FETCHERS[remoteSource](remoteSongId);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.handle('lyric-by-song', async (_event, song: QueueSong) => {
|
ipcMain.handle('lyric-by-song', async (_event, song: QueueSong) => {
|
||||||
const lyric = await getRemoteLyrics(song);
|
const lyric = await getRemoteLyrics(song);
|
||||||
return lyric;
|
return lyric;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('lyric-search', async (_event, params: LyricSearchQuery) => {
|
ipcMain.handle('lyric-search', async (_event, params: LyricSearchQuery) => {
|
||||||
const lyricResults = await searchRemoteLyrics(params);
|
const lyricResults = await searchRemoteLyrics(params);
|
||||||
return lyricResults;
|
return lyricResults;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('lyric-by-remote-id', async (_event, params: LyricGetQuery) => {
|
ipcMain.handle('lyric-by-remote-id', async (_event, params: LyricGetQuery) => {
|
||||||
const lyricResults = await getRemoteLyricsById(params);
|
const lyricResults = await getRemoteLyricsById(params);
|
||||||
return lyricResults;
|
return lyricResults;
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
|
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
} from '../../../../renderer/api/types';
|
} from '../../../../renderer/api/types';
|
||||||
import { orderSearchResults } from './shared';
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
@ -14,106 +14,106 @@ const SEEARCH_URL = 'https://lrclib.net/api/search';
|
|||||||
const TIMEOUT_MS = 5000;
|
const TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
export interface LrcLibSearchResponse {
|
export interface LrcLibSearchResponse {
|
||||||
albumName: string;
|
albumName: string;
|
||||||
artistName: string;
|
artistName: string;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LrcLibTrackResponse {
|
export interface LrcLibTrackResponse {
|
||||||
albumName: string;
|
albumName: string;
|
||||||
artistName: string;
|
artistName: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
id: number;
|
id: number;
|
||||||
instrumental: boolean;
|
instrumental: boolean;
|
||||||
isrc: string;
|
isrc: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
name: string;
|
name: string;
|
||||||
plainLyrics: string | null;
|
plainLyrics: string | null;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
spotifyId: string;
|
spotifyId: string;
|
||||||
syncedLyrics: string | null;
|
syncedLyrics: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function getSearchResults(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||||
let result: AxiosResponse<LrcLibSearchResponse[]>;
|
let result: AxiosResponse<LrcLibSearchResponse[]>;
|
||||||
|
|
||||||
if (!params.name) {
|
if (!params.name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await axios.get<LrcLibSearchResponse[]>(SEEARCH_URL, {
|
result = await axios.get<LrcLibSearchResponse[]>(SEEARCH_URL, {
|
||||||
params: {
|
params: {
|
||||||
q: params.name,
|
q: params.name,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('LrcLib search request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data) return null;
|
||||||
|
|
||||||
|
const songResults: InternetProviderLyricSearchResponse[] = result.data.map((song) => {
|
||||||
|
return {
|
||||||
|
artist: song.artistName,
|
||||||
|
id: String(song.id),
|
||||||
|
name: song.name,
|
||||||
|
source: LyricSource.LRCLIB,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
console.error('LrcLib search request got an error!', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.data) return null;
|
return orderSearchResults({ params, results: songResults });
|
||||||
|
|
||||||
const songResults: InternetProviderLyricSearchResponse[] = result.data.map((song) => {
|
|
||||||
return {
|
|
||||||
artist: song.artistName,
|
|
||||||
id: String(song.id),
|
|
||||||
name: song.name,
|
|
||||||
source: LyricSource.LRCLIB,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return orderSearchResults({ params, results: songResults });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
||||||
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('LrcLib lyrics request got an error!', e);
|
console.error('LrcLib lyrics request got an error!', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data.syncedLyrics || result.data.plainLyrics || null;
|
return result.data.syncedLyrics || result.data.plainLyrics || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricResponse | null> {
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await axios.get<LrcLibTrackResponse>(FETCH_URL, {
|
result = await axios.get<LrcLibTrackResponse>(FETCH_URL, {
|
||||||
params: {
|
params: {
|
||||||
album_name: params.album,
|
album_name: params.album,
|
||||||
artist_name: params.artist,
|
artist_name: params.artist,
|
||||||
duration: params.duration,
|
duration: params.duration,
|
||||||
track_name: params.name,
|
track_name: params.name,
|
||||||
},
|
},
|
||||||
timeout: TIMEOUT_MS,
|
timeout: TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('LrcLib search request got an error!', e);
|
console.error('LrcLib search request got an error!', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lyrics = result.data.syncedLyrics || result.data.plainLyrics || null;
|
const lyrics = result.data.syncedLyrics || result.data.plainLyrics || null;
|
||||||
|
|
||||||
if (!lyrics) {
|
if (!lyrics) {
|
||||||
console.error(`Could not get lyrics on LrcLib!`);
|
console.error(`Could not get lyrics on LrcLib!`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artist: result.data.artistName,
|
artist: result.data.artistName,
|
||||||
id: String(result.data.id),
|
id: String(result.data.id),
|
||||||
lyrics,
|
lyrics,
|
||||||
name: result.data.name,
|
name: result.data.name,
|
||||||
source: LyricSource.LRCLIB,
|
source: LyricSource.LRCLIB,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
import { LyricSource } from '../../../../renderer/api/types';
|
import { LyricSource } from '../../../../renderer/api/types';
|
||||||
import { orderSearchResults } from './shared';
|
import { orderSearchResults } from './shared';
|
||||||
import type {
|
import type {
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
|
|
||||||
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
||||||
@ -13,155 +13,155 @@ const LYRICS_URL = 'https://music.163.com/api/song/lyric';
|
|||||||
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
|
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
|
||||||
|
|
||||||
export interface NetEaseResponse {
|
export interface NetEaseResponse {
|
||||||
code: number;
|
code: number;
|
||||||
result: Result;
|
result: Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Result {
|
export interface Result {
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
songCount: number;
|
songCount: number;
|
||||||
songs: Song[];
|
songs: Song[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Song {
|
export interface Song {
|
||||||
album: Album;
|
album: Album;
|
||||||
alias: string[];
|
alias: string[];
|
||||||
artists: Artist[];
|
artists: Artist[];
|
||||||
copyrightId: number;
|
copyrightId: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
ftype: number;
|
ftype: number;
|
||||||
id: number;
|
id: number;
|
||||||
mark: number;
|
mark: number;
|
||||||
mvid: number;
|
mvid: number;
|
||||||
name: string;
|
name: string;
|
||||||
rUrl: null;
|
rUrl: null;
|
||||||
rtype: number;
|
rtype: number;
|
||||||
status: number;
|
status: number;
|
||||||
transNames?: string[];
|
transNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
artist: Artist;
|
artist: Artist;
|
||||||
copyrightId: number;
|
copyrightId: number;
|
||||||
id: number;
|
id: number;
|
||||||
mark: number;
|
mark: number;
|
||||||
name: string;
|
name: string;
|
||||||
picId: number;
|
picId: number;
|
||||||
publishTime: number;
|
publishTime: number;
|
||||||
size: number;
|
size: number;
|
||||||
status: number;
|
status: number;
|
||||||
transNames?: string[];
|
transNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Artist {
|
export interface Artist {
|
||||||
albumSize: number;
|
albumSize: number;
|
||||||
alias: any[];
|
alias: any[];
|
||||||
fansGroup: null;
|
fansGroup: null;
|
||||||
id: number;
|
id: number;
|
||||||
img1v1: number;
|
img1v1: number;
|
||||||
img1v1Url: string;
|
img1v1Url: string;
|
||||||
name: string;
|
name: string;
|
||||||
picId: number;
|
picId: number;
|
||||||
picUrl: null;
|
picUrl: null;
|
||||||
trans: null;
|
trans: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function getSearchResults(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||||
let result: AxiosResponse<NetEaseResponse>;
|
let result: AxiosResponse<NetEaseResponse>;
|
||||||
|
|
||||||
const searchQuery = [params.artist, params.name].join(' ');
|
const searchQuery = [params.artist, params.name].join(' ');
|
||||||
|
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await axios.get(SEARCH_URL, {
|
result = await axios.get(SEARCH_URL, {
|
||||||
params: {
|
params: {
|
||||||
limit: 5,
|
limit: 5,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
s: searchQuery,
|
s: searchQuery,
|
||||||
type: '1',
|
type: '1',
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('NetEase search request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawSongsResult = result?.data.result?.songs;
|
||||||
|
|
||||||
|
if (!rawSongsResult) return null;
|
||||||
|
|
||||||
|
const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map((song) => {
|
||||||
|
const artist = song.artists ? song.artists.map((artist) => artist.name).join(', ') : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
artist,
|
||||||
|
id: String(song.id),
|
||||||
|
name: song.name,
|
||||||
|
source: LyricSource.NETEASE,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
console.error('NetEase search request got an error!', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawSongsResult = result?.data.result?.songs;
|
return orderSearchResults({ params, results: songResults });
|
||||||
|
|
||||||
if (!rawSongsResult) return null;
|
|
||||||
|
|
||||||
const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map((song) => {
|
|
||||||
const artist = song.artists ? song.artists.map((artist) => artist.name).join(', ') : '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
artist,
|
|
||||||
id: String(song.id),
|
|
||||||
name: song.name,
|
|
||||||
source: LyricSource.NETEASE,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return orderSearchResults({ params, results: songResults });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMatchedLyrics(
|
async function getMatchedLyrics(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
|
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
|
||||||
const results = await getSearchResults(params);
|
const results = await getSearchResults(params);
|
||||||
|
|
||||||
const firstMatch = results?.[0];
|
const firstMatch = results?.[0];
|
||||||
|
|
||||||
if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) {
|
if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return firstMatch;
|
return firstMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
||||||
let result: AxiosResponse<any, any>;
|
let result: AxiosResponse<any, any>;
|
||||||
try {
|
try {
|
||||||
result = await axios.get(LYRICS_URL, {
|
result = await axios.get(LYRICS_URL, {
|
||||||
params: {
|
params: {
|
||||||
id: songId,
|
id: songId,
|
||||||
kv: '-1',
|
kv: '-1',
|
||||||
lv: '-1',
|
lv: '-1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('NetEase lyrics request got an error!', e);
|
console.error('NetEase lyrics request got an error!', e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data.klyric?.lyric || result.data.lrc?.lyric;
|
return result.data.klyric?.lyric || result.data.lrc?.lyric;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricResponse | null> {
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
const lyricsMatch = await getMatchedLyrics(params);
|
const lyricsMatch = await getMatchedLyrics(params);
|
||||||
if (!lyricsMatch) {
|
if (!lyricsMatch) {
|
||||||
console.error('Could not find the song on NetEase!');
|
console.error('Could not find the song on NetEase!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lyrics = await getLyricsBySongId(lyricsMatch.id);
|
const lyrics = await getLyricsBySongId(lyricsMatch.id);
|
||||||
if (!lyrics) {
|
if (!lyrics) {
|
||||||
console.error('Could not get lyrics on NetEase!');
|
console.error('Could not get lyrics on NetEase!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artist: lyricsMatch.artist,
|
artist: lyricsMatch.artist,
|
||||||
id: lyricsMatch.id,
|
id: lyricsMatch.id,
|
||||||
lyrics,
|
lyrics,
|
||||||
name: lyricsMatch.name,
|
name: lyricsMatch.name,
|
||||||
source: LyricSource.NETEASE,
|
source: LyricSource.NETEASE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
} from '../../../../renderer/api/types';
|
} from '../../../../renderer/api/types';
|
||||||
|
|
||||||
export const orderSearchResults = (args: {
|
export const orderSearchResults = (args: {
|
||||||
params: LyricSearchQuery;
|
params: LyricSearchQuery;
|
||||||
results: InternetProviderLyricSearchResponse[];
|
results: InternetProviderLyricSearchResponse[];
|
||||||
}) => {
|
}) => {
|
||||||
const { params, results } = args;
|
const { params, results } = args;
|
||||||
|
|
||||||
const options: Fuse.IFuseOptions<InternetProviderLyricSearchResponse> = {
|
const options: Fuse.IFuseOptions<InternetProviderLyricSearchResponse> = {
|
||||||
fieldNormWeight: 1,
|
fieldNormWeight: 1,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
keys: [
|
keys: [
|
||||||
{ getFn: (song) => song.name, name: 'name', weight: 3 },
|
{ getFn: (song) => song.name, name: 'name', weight: 3 },
|
||||||
{ getFn: (song) => song.artist, name: 'artist' },
|
{ getFn: (song) => song.artist, name: 'artist' },
|
||||||
],
|
],
|
||||||
threshold: 1.0,
|
threshold: 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fuse = new Fuse(results, options);
|
const fuse = new Fuse(results, options);
|
||||||
|
|
||||||
const searchResults = fuse.search<InternetProviderLyricSearchResponse>({
|
const searchResults = fuse.search<InternetProviderLyricSearchResponse>({
|
||||||
...(params.artist && { artist: params.artist }),
|
...(params.artist && { artist: params.artist }),
|
||||||
...(params.name && { name: params.name }),
|
...(params.name && { name: params.name }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return searchResults.map((result) => ({
|
return searchResults.map((result) => ({
|
||||||
...result.item,
|
...result.item,
|
||||||
score: result.score,
|
score: result.score,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -6,215 +6,215 @@ import { PlayerData } from '/@/renderer/store';
|
|||||||
declare module 'node-mpv';
|
declare module 'node-mpv';
|
||||||
|
|
||||||
function wait(timeout: number) {
|
function wait(timeout: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve('resolved');
|
resolve('resolved');
|
||||||
}, timeout);
|
}, timeout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('player-is-running', async () => {
|
ipcMain.handle('player-is-running', async () => {
|
||||||
return getMpvInstance()?.isRunning();
|
return getMpvInstance()?.isRunning();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('player-clean-up', async () => {
|
ipcMain.handle('player-clean-up', async () => {
|
||||||
getMpvInstance()?.stop();
|
getMpvInstance()?.stop();
|
||||||
getMpvInstance()?.clearPlaylist();
|
getMpvInstance()?.clearPlaylist();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('player-start', async () => {
|
ipcMain.on('player-start', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.play()
|
?.play()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to play', err);
|
console.log('MPV failed to play', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Starts the player
|
// Starts the player
|
||||||
ipcMain.on('player-play', async () => {
|
ipcMain.on('player-play', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.play()
|
?.play()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to play', err);
|
console.log('MPV failed to play', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pauses the player
|
// Pauses the player
|
||||||
ipcMain.on('player-pause', async () => {
|
ipcMain.on('player-pause', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.pause()
|
?.pause()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to pause', err);
|
console.log('MPV failed to pause', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stops the player
|
// Stops the player
|
||||||
ipcMain.on('player-stop', async () => {
|
ipcMain.on('player-stop', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.stop()
|
?.stop()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to stop', err);
|
console.log('MPV failed to stop', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Goes to the next track in the playlist
|
// Goes to the next track in the playlist
|
||||||
ipcMain.on('player-next', async () => {
|
ipcMain.on('player-next', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.next()
|
?.next()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to go to next', err);
|
console.log('MPV failed to go to next', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Goes to the previous track in the playlist
|
// Goes to the previous track in the playlist
|
||||||
ipcMain.on('player-previous', async () => {
|
ipcMain.on('player-previous', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.prev()
|
?.prev()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to go to previous', err);
|
console.log('MPV failed to go to previous', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seeks forward or backward by the given amount of seconds
|
// Seeks forward or backward by the given amount of seconds
|
||||||
ipcMain.on('player-seek', async (_event, time: number) => {
|
ipcMain.on('player-seek', async (_event, time: number) => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.seek(time)
|
?.seek(time)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to seek', err);
|
console.log('MPV failed to seek', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seeks to the given time in seconds
|
// Seeks to the given time in seconds
|
||||||
ipcMain.on('player-seek-to', async (_event, time: number) => {
|
ipcMain.on('player-seek-to', async (_event, time: number) => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.goToPosition(time)
|
?.goToPosition(time)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(`MPV failed to seek to ${time}`, err);
|
console.log(`MPV failed to seek to ${time}`, err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
|
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
|
||||||
ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
|
ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
|
||||||
if (!data.queue.current && !data.queue.next) {
|
if (!data.queue.current && !data.queue.next) {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.clearPlaylist()
|
?.clearPlaylist()
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to clear playlist', err);
|
|
||||||
});
|
|
||||||
await getMpvInstance()
|
|
||||||
?.pause()
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to pause', err);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let complete = false;
|
|
||||||
let tryAttempts = 0;
|
|
||||||
|
|
||||||
while (!complete) {
|
|
||||||
if (tryAttempts > 3) {
|
|
||||||
getMainWindow()?.webContents.send('renderer-player-error', 'Failed to load song');
|
|
||||||
complete = true;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
if (data.queue.current) {
|
|
||||||
await getMpvInstance()
|
|
||||||
?.load(data.queue.current.streamUrl, 'replace')
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to load song', err);
|
console.log('MPV failed to clear playlist', err);
|
||||||
});
|
});
|
||||||
}
|
await getMpvInstance()
|
||||||
|
?.pause()
|
||||||
if (data.queue.next) {
|
|
||||||
await getMpvInstance()
|
|
||||||
?.load(data.queue.next.streamUrl, 'append')
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to load next song', err);
|
console.log('MPV failed to pause', err);
|
||||||
});
|
});
|
||||||
}
|
return;
|
||||||
|
|
||||||
complete = true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
tryAttempts += 1;
|
|
||||||
await wait(500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (pause) {
|
let complete = false;
|
||||||
await getMpvInstance()?.pause();
|
let tryAttempts = 0;
|
||||||
}
|
|
||||||
|
while (!complete) {
|
||||||
|
if (tryAttempts > 3) {
|
||||||
|
getMainWindow()?.webContents.send('renderer-player-error', 'Failed to load song');
|
||||||
|
complete = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (data.queue.current) {
|
||||||
|
await getMpvInstance()
|
||||||
|
?.load(data.queue.current.streamUrl, 'replace')
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('MPV failed to load song', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.queue.next) {
|
||||||
|
await getMpvInstance()
|
||||||
|
?.load(data.queue.next.streamUrl, 'append')
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('MPV failed to load next song', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
complete = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
tryAttempts += 1;
|
||||||
|
await wait(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pause) {
|
||||||
|
await getMpvInstance()?.pause();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replaces the queue in position 1 to the given data
|
// Replaces the queue in position 1 to the given data
|
||||||
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
||||||
const size = await getMpvInstance()
|
const size = await getMpvInstance()
|
||||||
?.getPlaylistSize()
|
?.getPlaylistSize()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to get playlist size', err);
|
console.log('MPV failed to get playlist size', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!size) {
|
if (!size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size > 1) {
|
if (size > 1) {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.playlistRemove(1)
|
?.playlistRemove(1)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to remove song from playlist', err);
|
console.log('MPV failed to remove song from playlist', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.queue.next) {
|
if (data.queue.next) {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.load(data.queue.next.streamUrl, 'append')
|
?.load(data.queue.next.streamUrl, 'append')
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to load next song', err);
|
console.log('MPV failed to load next song', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets the next song in the queue when reaching the end of the queue
|
// Sets the next song in the queue when reaching the end of the queue
|
||||||
ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
|
ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
|
||||||
// Always keep the current song as position 0 in the mpv queue
|
// Always keep the current song as position 0 in the mpv queue
|
||||||
// This allows us to easily set update the next song in the queue without
|
// This allows us to easily set update the next song in the queue without
|
||||||
// disturbing the currently playing song
|
// disturbing the currently playing song
|
||||||
await getMpvInstance()
|
|
||||||
?.playlistRemove(0)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to remove song from playlist', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.queue.next) {
|
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.load(data.queue.next.streamUrl, 'append')
|
?.playlistRemove(0)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to load next song', err);
|
console.log('MPV failed to remove song from playlist', err);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
if (data.queue.next) {
|
||||||
|
await getMpvInstance()
|
||||||
|
?.load(data.queue.next.streamUrl, 'append')
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('MPV failed to load next song', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets the volume to the given value (0-100)
|
// Sets the volume to the given value (0-100)
|
||||||
ipcMain.on('player-volume', async (_event, value: number) => {
|
ipcMain.on('player-volume', async (_event, value: number) => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.volume(value)
|
?.volume(value)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to set volume', err);
|
console.log('MPV failed to set volume', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggles the mute status
|
// Toggles the mute status
|
||||||
ipcMain.on('player-mute', async () => {
|
ipcMain.on('player-mute', async () => {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.mute()
|
?.mute()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log('MPV failed to toggle mute', err);
|
console.log('MPV failed to toggle mute', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
||||||
return getMpvInstance()?.getTimePosition();
|
return getMpvInstance()?.getTimePosition();
|
||||||
});
|
});
|
||||||
|
@ -2,26 +2,26 @@
|
|||||||
import { BrowserWindow, globalShortcut } from 'electron';
|
import { BrowserWindow, globalShortcut } from 'electron';
|
||||||
|
|
||||||
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||||
globalShortcut.register('MediaStop', () => {
|
globalShortcut.register('MediaStop', () => {
|
||||||
window?.webContents.send('renderer-player-stop');
|
window?.webContents.send('renderer-player-stop');
|
||||||
});
|
});
|
||||||
|
|
||||||
globalShortcut.register('MediaPlayPause', () => {
|
globalShortcut.register('MediaPlayPause', () => {
|
||||||
window?.webContents.send('renderer-player-play-pause');
|
window?.webContents.send('renderer-player-play-pause');
|
||||||
});
|
});
|
||||||
|
|
||||||
globalShortcut.register('MediaNextTrack', () => {
|
globalShortcut.register('MediaNextTrack', () => {
|
||||||
window?.webContents.send('renderer-player-next');
|
window?.webContents.send('renderer-player-next');
|
||||||
});
|
});
|
||||||
|
|
||||||
globalShortcut.register('MediaPreviousTrack', () => {
|
globalShortcut.register('MediaPreviousTrack', () => {
|
||||||
window?.webContents.send('renderer-player-previous');
|
window?.webContents.send('renderer-player-previous');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const disableMediaKeys = () => {
|
export const disableMediaKeys = () => {
|
||||||
globalShortcut.unregister('MediaStop');
|
globalShortcut.unregister('MediaStop');
|
||||||
globalShortcut.unregister('MediaPlayPause');
|
globalShortcut.unregister('MediaPlayPause');
|
||||||
globalShortcut.unregister('MediaNextTrack');
|
globalShortcut.unregister('MediaNextTrack');
|
||||||
globalShortcut.unregister('MediaPreviousTrack');
|
globalShortcut.unregister('MediaPreviousTrack');
|
||||||
};
|
};
|
||||||
|
@ -4,47 +4,47 @@ import Store from 'electron-store';
|
|||||||
export const store = new Store();
|
export const store = new Store();
|
||||||
|
|
||||||
ipcMain.handle('settings-get', (_event, data: { property: string }) => {
|
ipcMain.handle('settings-get', (_event, data: { property: string }) => {
|
||||||
return store.get(`${data.property}`);
|
return store.get(`${data.property}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('settings-set', (__event, data: { property: string; value: any }) => {
|
ipcMain.on('settings-set', (__event, data: { property: string; value: any }) => {
|
||||||
store.set(`${data.property}`, data.value);
|
store.set(`${data.property}`, data.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('password-get', (_event, server: string): string | null => {
|
ipcMain.handle('password-get', (_event, server: string): string | null => {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
const servers = store.get('server') as Record<string, string> | undefined;
|
const servers = store.get('server') as Record<string, string> | undefined;
|
||||||
|
|
||||||
if (!servers) {
|
if (!servers) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = servers[server];
|
||||||
|
if (!encrypted) return null;
|
||||||
|
|
||||||
|
const decrypted = safeStorage.decryptString(Buffer.from(encrypted, 'hex'));
|
||||||
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encrypted = servers[server];
|
return null;
|
||||||
if (!encrypted) return null;
|
|
||||||
|
|
||||||
const decrypted = safeStorage.decryptString(Buffer.from(encrypted, 'hex'));
|
|
||||||
return decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('password-remove', (_event, server: string) => {
|
ipcMain.on('password-remove', (_event, server: string) => {
|
||||||
const passwords = store.get('server', {}) as Record<string, string>;
|
const passwords = store.get('server', {}) as Record<string, string>;
|
||||||
if (server in passwords) {
|
if (server in passwords) {
|
||||||
delete passwords[server];
|
delete passwords[server];
|
||||||
}
|
}
|
||||||
store.set({ server: passwords });
|
store.set({ server: passwords });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('password-set', (_event, password: string, server: string) => {
|
ipcMain.handle('password-set', (_event, password: string, server: string) => {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
const encrypted = safeStorage.encryptString(password);
|
const encrypted = safeStorage.encryptString(password);
|
||||||
const passwords = store.get('server', {}) as Record<string, string>;
|
const passwords = store.get('server', {}) as Record<string, string>;
|
||||||
passwords[server] = encrypted.toString('hex');
|
passwords[server] = encrypted.toString('hex');
|
||||||
store.set({ server: passwords });
|
store.set({ server: passwords });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
@ -5,164 +5,166 @@ import { getMainWindow } from '../../main';
|
|||||||
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
|
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
|
||||||
|
|
||||||
const mprisPlayer = Player({
|
const mprisPlayer = Player({
|
||||||
identity: 'Feishin',
|
identity: 'Feishin',
|
||||||
maximumRate: 1.0,
|
maximumRate: 1.0,
|
||||||
minimumRate: 1.0,
|
minimumRate: 1.0,
|
||||||
name: 'Feishin',
|
name: 'Feishin',
|
||||||
rate: 1.0,
|
rate: 1.0,
|
||||||
supportedInterfaces: ['player'],
|
supportedInterfaces: ['player'],
|
||||||
supportedMimeTypes: ['audio/mpeg', 'application/ogg'],
|
supportedMimeTypes: ['audio/mpeg', 'application/ogg'],
|
||||||
supportedUriSchemes: ['file'],
|
supportedUriSchemes: ['file'],
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('quit', () => {
|
mprisPlayer.on('quit', () => {
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('stop', () => {
|
mprisPlayer.on('stop', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-stop');
|
getMainWindow()?.webContents.send('renderer-player-stop');
|
||||||
mprisPlayer.playbackStatus = 'Paused';
|
mprisPlayer.playbackStatus = 'Paused';
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('pause', () => {
|
mprisPlayer.on('pause', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-pause');
|
getMainWindow()?.webContents.send('renderer-player-pause');
|
||||||
mprisPlayer.playbackStatus = 'Paused';
|
mprisPlayer.playbackStatus = 'Paused';
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('play', () => {
|
mprisPlayer.on('play', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-play');
|
getMainWindow()?.webContents.send('renderer-player-play');
|
||||||
mprisPlayer.playbackStatus = 'Playing';
|
mprisPlayer.playbackStatus = 'Playing';
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('playpause', () => {
|
mprisPlayer.on('playpause', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-play-pause');
|
getMainWindow()?.webContents.send('renderer-player-play-pause');
|
||||||
if (mprisPlayer.playbackStatus !== 'Playing') {
|
if (mprisPlayer.playbackStatus !== 'Playing') {
|
||||||
mprisPlayer.playbackStatus = 'Playing';
|
mprisPlayer.playbackStatus = 'Playing';
|
||||||
} else {
|
} else {
|
||||||
mprisPlayer.playbackStatus = 'Paused';
|
mprisPlayer.playbackStatus = 'Paused';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('next', () => {
|
mprisPlayer.on('next', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-next');
|
getMainWindow()?.webContents.send('renderer-player-next');
|
||||||
|
|
||||||
if (mprisPlayer.playbackStatus !== 'Playing') {
|
if (mprisPlayer.playbackStatus !== 'Playing') {
|
||||||
mprisPlayer.playbackStatus = 'Playing';
|
mprisPlayer.playbackStatus = 'Playing';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('previous', () => {
|
mprisPlayer.on('previous', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-previous');
|
getMainWindow()?.webContents.send('renderer-player-previous');
|
||||||
|
|
||||||
if (mprisPlayer.playbackStatus !== 'Playing') {
|
if (mprisPlayer.playbackStatus !== 'Playing') {
|
||||||
mprisPlayer.playbackStatus = Player.PLAYBACK_STATUS_PLAYING;
|
mprisPlayer.playbackStatus = Player.PLAYBACK_STATUS_PLAYING;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('volume', (event: any) => {
|
mprisPlayer.on('volume', (event: any) => {
|
||||||
getMainWindow()?.webContents.send('mpris-request-volume', {
|
getMainWindow()?.webContents.send('mpris-request-volume', {
|
||||||
volume: event,
|
volume: event,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('shuffle', (event: boolean) => {
|
mprisPlayer.on('shuffle', (event: boolean) => {
|
||||||
getMainWindow()?.webContents.send('mpris-request-toggle-shuffle', { shuffle: event });
|
getMainWindow()?.webContents.send('mpris-request-toggle-shuffle', { shuffle: event });
|
||||||
mprisPlayer.shuffle = event;
|
mprisPlayer.shuffle = event;
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('loopStatus', (event: string) => {
|
mprisPlayer.on('loopStatus', (event: string) => {
|
||||||
getMainWindow()?.webContents.send('mpris-request-toggle-repeat', { repeat: event });
|
getMainWindow()?.webContents.send('mpris-request-toggle-repeat', { repeat: event });
|
||||||
mprisPlayer.loopStatus = event;
|
mprisPlayer.loopStatus = event;
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('position', (event: any) => {
|
mprisPlayer.on('position', (event: any) => {
|
||||||
getMainWindow()?.webContents.send('mpris-request-position', {
|
getMainWindow()?.webContents.send('mpris-request-position', {
|
||||||
position: event.position / 1e6,
|
position: event.position / 1e6,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('seek', (event: number) => {
|
mprisPlayer.on('seek', (event: number) => {
|
||||||
getMainWindow()?.webContents.send('mpris-request-seek', {
|
getMainWindow()?.webContents.send('mpris-request-seek', {
|
||||||
offset: event / 1e6,
|
offset: event / 1e6,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('mpris-update-position', (_event, arg) => {
|
ipcMain.on('mpris-update-position', (_event, arg) => {
|
||||||
mprisPlayer.getPosition = () => arg * 1e6;
|
mprisPlayer.getPosition = () => arg * 1e6;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('mpris-update-seek', (_event, arg) => {
|
ipcMain.on('mpris-update-seek', (_event, arg) => {
|
||||||
mprisPlayer.seeked(arg * 1e6);
|
mprisPlayer.seeked(arg * 1e6);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('mpris-update-volume', (_event, arg) => {
|
ipcMain.on('mpris-update-volume', (_event, arg) => {
|
||||||
mprisPlayer.volume = Number(arg);
|
mprisPlayer.volume = Number(arg);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('mpris-update-repeat', (_event, arg) => {
|
ipcMain.on('mpris-update-repeat', (_event, arg) => {
|
||||||
mprisPlayer.loopStatus = arg;
|
mprisPlayer.loopStatus = arg;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('mpris-update-shuffle', (_event, arg) => {
|
ipcMain.on('mpris-update-shuffle', (_event, arg) => {
|
||||||
mprisPlayer.shuffle = arg;
|
mprisPlayer.shuffle = arg;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on(
|
ipcMain.on(
|
||||||
'mpris-update-song',
|
'mpris-update-song',
|
||||||
(
|
(
|
||||||
_event,
|
_event,
|
||||||
args: {
|
args: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
repeat: PlayerRepeat;
|
repeat: PlayerRepeat;
|
||||||
shuffle: PlayerShuffle;
|
shuffle: PlayerShuffle;
|
||||||
song: QueueSong;
|
song: QueueSong;
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { song, status, repeat, shuffle } = args || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
mprisPlayer.playbackStatus = status;
|
||||||
|
|
||||||
|
if (repeat) {
|
||||||
|
mprisPlayer.loopStatus =
|
||||||
|
repeat === 'all' ? 'Playlist' : repeat === 'one' ? 'Track' : 'None';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shuffle) {
|
||||||
|
mprisPlayer.shuffle = shuffle !== 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!song) return;
|
||||||
|
|
||||||
|
const upsizedImageUrl = song.imageUrl
|
||||||
|
? song.imageUrl
|
||||||
|
?.replace(/&size=\d+/, '&size=300')
|
||||||
|
.replace(/\?width=\d+/, '?width=300')
|
||||||
|
.replace(/&height=\d+/, '&height=300')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
mprisPlayer.metadata = {
|
||||||
|
'mpris:artUrl': upsizedImageUrl,
|
||||||
|
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e6) : null,
|
||||||
|
'mpris:trackid': song?.id
|
||||||
|
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
||||||
|
: '',
|
||||||
|
'xesam:album': song.album || null,
|
||||||
|
'xesam:albumArtist': song.albumArtists?.length ? song.albumArtists[0].name : null,
|
||||||
|
'xesam:artist':
|
||||||
|
song.artists?.length !== 0
|
||||||
|
? song.artists?.map((artist: RelatedArtist) => artist.name)
|
||||||
|
: null,
|
||||||
|
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
||||||
|
'xesam:genre': song.genres?.length
|
||||||
|
? song.genres.map((genre: any) => genre.name)
|
||||||
|
: null,
|
||||||
|
'xesam:title': song.name || null,
|
||||||
|
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
||||||
|
'xesam:useCount':
|
||||||
|
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) => {
|
|
||||||
const { song, status, repeat, shuffle } = args || {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
mprisPlayer.playbackStatus = status;
|
|
||||||
|
|
||||||
if (repeat) {
|
|
||||||
mprisPlayer.loopStatus =
|
|
||||||
repeat === 'all' ? 'Playlist' : repeat === 'one' ? 'Track' : 'None';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shuffle) {
|
|
||||||
mprisPlayer.shuffle = shuffle !== 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!song) return;
|
|
||||||
|
|
||||||
const upsizedImageUrl = song.imageUrl
|
|
||||||
? song.imageUrl
|
|
||||||
?.replace(/&size=\d+/, '&size=300')
|
|
||||||
.replace(/\?width=\d+/, '?width=300')
|
|
||||||
.replace(/&height=\d+/, '&height=300')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
mprisPlayer.metadata = {
|
|
||||||
'mpris:artUrl': upsizedImageUrl,
|
|
||||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e6) : null,
|
|
||||||
'mpris:trackid': song?.id
|
|
||||||
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
|
||||||
: '',
|
|
||||||
'xesam:album': song.album || null,
|
|
||||||
'xesam:albumArtist': song.albumArtists?.length ? song.albumArtists[0].name : null,
|
|
||||||
'xesam:artist':
|
|
||||||
song.artists?.length !== 0
|
|
||||||
? song.artists?.map((artist: RelatedArtist) => artist.name)
|
|
||||||
: null,
|
|
||||||
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
|
||||||
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
|
|
||||||
'xesam:title': song.name || null,
|
|
||||||
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
|
||||||
'xesam:useCount':
|
|
||||||
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
993
src/main/main.ts
993
src/main/main.ts
File diff suppressed because it is too large
Load Diff
500
src/main/menu.ts
500
src/main/menu.ts
@ -1,269 +1,279 @@
|
|||||||
import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
||||||
selector?: string;
|
selector?: string;
|
||||||
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MenuBuilder {
|
export default class MenuBuilder {
|
||||||
mainWindow: BrowserWindow;
|
mainWindow: BrowserWindow;
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow) {
|
constructor(mainWindow: BrowserWindow) {
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
}
|
|
||||||
|
|
||||||
buildMenu(): Menu {
|
|
||||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
|
||||||
this.setupDevelopmentEnvironment();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const template =
|
buildMenu(): Menu {
|
||||||
process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate();
|
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||||
|
this.setupDevelopmentEnvironment();
|
||||||
|
}
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const template =
|
||||||
Menu.setApplicationMenu(menu);
|
process.platform === 'darwin'
|
||||||
|
? this.buildDarwinTemplate()
|
||||||
|
: this.buildDefaultTemplate();
|
||||||
|
|
||||||
return menu;
|
const menu = Menu.buildFromTemplate(template);
|
||||||
}
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
setupDevelopmentEnvironment(): void {
|
return menu;
|
||||||
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
}
|
||||||
const { x, y } = props;
|
|
||||||
|
|
||||||
Menu.buildFromTemplate([
|
setupDevelopmentEnvironment(): void {
|
||||||
{
|
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
||||||
click: () => {
|
const { x, y } = props;
|
||||||
this.mainWindow.webContents.inspectElement(x, y);
|
|
||||||
},
|
|
||||||
label: 'Inspect element',
|
|
||||||
},
|
|
||||||
]).popup({ window: this.mainWindow });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
Menu.buildFromTemplate([
|
||||||
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
|
||||||
label: 'Electron',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'About ElectronReact',
|
|
||||||
selector: 'orderFrontStandardAboutPanel:',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ label: 'Services', submenu: [] },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
accelerator: 'Command+H',
|
|
||||||
label: 'Hide ElectronReact',
|
|
||||||
selector: 'hide:',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: 'Command+Shift+H',
|
|
||||||
label: 'Hide Others',
|
|
||||||
selector: 'hideOtherApplications:',
|
|
||||||
},
|
|
||||||
{ label: 'Show All', selector: 'unhideAllApplications:' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
accelerator: 'Command+Q',
|
|
||||||
click: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
label: 'Quit',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const subMenuEdit: DarwinMenuItemConstructorOptions = {
|
|
||||||
label: 'Edit',
|
|
||||||
submenu: [
|
|
||||||
{ accelerator: 'Command+Z', label: 'Undo', selector: 'undo:' },
|
|
||||||
{ accelerator: 'Shift+Command+Z', label: 'Redo', selector: 'redo:' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ accelerator: 'Command+X', label: 'Cut', selector: 'cut:' },
|
|
||||||
{ accelerator: 'Command+C', label: 'Copy', selector: 'copy:' },
|
|
||||||
{ accelerator: 'Command+V', label: 'Paste', selector: 'paste:' },
|
|
||||||
{
|
|
||||||
accelerator: 'Command+A',
|
|
||||||
label: 'Select All',
|
|
||||||
selector: 'selectAll:',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const subMenuViewDev: MenuItemConstructorOptions = {
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
accelerator: 'Command+R',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.reload();
|
|
||||||
},
|
|
||||||
label: 'Reload',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: 'Ctrl+Command+F',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
|
||||||
},
|
|
||||||
label: 'Toggle Full Screen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: 'Alt+Command+I',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.toggleDevTools();
|
|
||||||
},
|
|
||||||
label: 'Toggle Developer Tools',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const subMenuViewProd: MenuItemConstructorOptions = {
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
accelerator: 'Ctrl+Command+F',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
|
||||||
},
|
|
||||||
label: 'Toggle Full Screen',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const subMenuWindow: DarwinMenuItemConstructorOptions = {
|
|
||||||
label: 'Window',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
accelerator: 'Command+M',
|
|
||||||
label: 'Minimize',
|
|
||||||
selector: 'performMiniaturize:',
|
|
||||||
},
|
|
||||||
{ accelerator: 'Command+W', label: 'Close', selector: 'performClose:' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const subMenuHelp: MenuItemConstructorOptions = {
|
|
||||||
label: 'Help',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
click() {
|
|
||||||
shell.openExternal('https://electronjs.org');
|
|
||||||
},
|
|
||||||
label: 'Learn More',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
click() {
|
|
||||||
shell.openExternal('https://github.com/electron/electron/tree/main/docs#readme');
|
|
||||||
},
|
|
||||||
label: 'Documentation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
click() {
|
|
||||||
shell.openExternal('https://www.electronjs.org/community');
|
|
||||||
},
|
|
||||||
label: 'Community Discussions',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
click() {
|
|
||||||
shell.openExternal('https://github.com/electron/electron/issues');
|
|
||||||
},
|
|
||||||
label: 'Search Issues',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const subMenuView =
|
|
||||||
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'
|
|
||||||
? subMenuViewDev
|
|
||||||
: subMenuViewProd;
|
|
||||||
|
|
||||||
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
|
||||||
}
|
|
||||||
|
|
||||||
buildDefaultTemplate() {
|
|
||||||
const templateDefault = [
|
|
||||||
{
|
|
||||||
label: '&File',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
accelerator: 'Ctrl+O',
|
|
||||||
label: '&Open',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accelerator: 'Ctrl+W',
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.close();
|
|
||||||
},
|
|
||||||
label: '&Close',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '&View',
|
|
||||||
submenu:
|
|
||||||
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'
|
|
||||||
? [
|
|
||||||
{
|
{
|
||||||
accelerator: 'Ctrl+R',
|
click: () => {
|
||||||
click: () => {
|
this.mainWindow.webContents.inspectElement(x, y);
|
||||||
this.mainWindow.webContents.reload();
|
},
|
||||||
},
|
label: 'Inspect element',
|
||||||
label: '&Reload',
|
},
|
||||||
|
]).popup({ window: this.mainWindow });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
||||||
|
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
||||||
|
label: 'Electron',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'About ElectronReact',
|
||||||
|
selector: 'orderFrontStandardAboutPanel:',
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Services', submenu: [] },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
accelerator: 'Command+H',
|
||||||
|
label: 'Hide ElectronReact',
|
||||||
|
selector: 'hide:',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accelerator: 'F11',
|
accelerator: 'Command+Shift+H',
|
||||||
click: () => {
|
label: 'Hide Others',
|
||||||
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
selector: 'hideOtherApplications:',
|
||||||
},
|
},
|
||||||
label: 'Toggle &Full Screen',
|
{ label: 'Show All', selector: 'unhideAllApplications:' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
accelerator: 'Command+Q',
|
||||||
|
click: () => {
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
label: 'Quit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuEdit: DarwinMenuItemConstructorOptions = {
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ accelerator: 'Command+Z', label: 'Undo', selector: 'undo:' },
|
||||||
|
{ accelerator: 'Shift+Command+Z', label: 'Redo', selector: 'redo:' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ accelerator: 'Command+X', label: 'Cut', selector: 'cut:' },
|
||||||
|
{ accelerator: 'Command+C', label: 'Copy', selector: 'copy:' },
|
||||||
|
{ accelerator: 'Command+V', label: 'Paste', selector: 'paste:' },
|
||||||
|
{
|
||||||
|
accelerator: 'Command+A',
|
||||||
|
label: 'Select All',
|
||||||
|
selector: 'selectAll:',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const subMenuViewDev: MenuItemConstructorOptions = {
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
accelerator: 'Command+R',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.reload();
|
||||||
|
},
|
||||||
|
label: 'Reload',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accelerator: 'Alt+Ctrl+I',
|
accelerator: 'Ctrl+Command+F',
|
||||||
click: () => {
|
click: () => {
|
||||||
this.mainWindow.webContents.toggleDevTools();
|
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
||||||
},
|
},
|
||||||
label: 'Toggle &Developer Tools',
|
label: 'Toggle Full Screen',
|
||||||
},
|
},
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
{
|
||||||
accelerator: 'F11',
|
accelerator: 'Alt+Command+I',
|
||||||
click: () => {
|
click: () => {
|
||||||
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
this.mainWindow.webContents.toggleDevTools();
|
||||||
},
|
},
|
||||||
label: 'Toggle &Full Screen',
|
label: 'Toggle Developer Tools',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
{
|
const subMenuViewProd: MenuItemConstructorOptions = {
|
||||||
label: 'Help',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
click() {
|
accelerator: 'Ctrl+Command+F',
|
||||||
shell.openExternal('https://electronjs.org');
|
click: () => {
|
||||||
},
|
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
||||||
label: 'Learn More',
|
},
|
||||||
},
|
label: 'Toggle Full Screen',
|
||||||
{
|
},
|
||||||
click() {
|
],
|
||||||
shell.openExternal('https://github.com/electron/electron/tree/main/docs#readme');
|
};
|
||||||
},
|
const subMenuWindow: DarwinMenuItemConstructorOptions = {
|
||||||
label: 'Documentation',
|
label: 'Window',
|
||||||
},
|
submenu: [
|
||||||
{
|
{
|
||||||
click() {
|
accelerator: 'Command+M',
|
||||||
shell.openExternal('https://www.electronjs.org/community');
|
label: 'Minimize',
|
||||||
},
|
selector: 'performMiniaturize:',
|
||||||
label: 'Community Discussions',
|
},
|
||||||
},
|
{ accelerator: 'Command+W', label: 'Close', selector: 'performClose:' },
|
||||||
{
|
{ type: 'separator' },
|
||||||
click() {
|
{ label: 'Bring All to Front', selector: 'arrangeInFront:' },
|
||||||
shell.openExternal('https://github.com/electron/electron/issues');
|
],
|
||||||
},
|
};
|
||||||
label: 'Search Issues',
|
const subMenuHelp: MenuItemConstructorOptions = {
|
||||||
},
|
label: 'Help',
|
||||||
],
|
submenu: [
|
||||||
},
|
{
|
||||||
];
|
click() {
|
||||||
|
shell.openExternal('https://electronjs.org');
|
||||||
|
},
|
||||||
|
label: 'Learn More',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal(
|
||||||
|
'https://github.com/electron/electron/tree/main/docs#readme',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://www.electronjs.org/community');
|
||||||
|
},
|
||||||
|
label: 'Community Discussions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://github.com/electron/electron/issues');
|
||||||
|
},
|
||||||
|
label: 'Search Issues',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return templateDefault;
|
const subMenuView =
|
||||||
}
|
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'
|
||||||
|
? subMenuViewDev
|
||||||
|
: subMenuViewProd;
|
||||||
|
|
||||||
|
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDefaultTemplate() {
|
||||||
|
const templateDefault = [
|
||||||
|
{
|
||||||
|
label: '&File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
accelerator: 'Ctrl+O',
|
||||||
|
label: '&Open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accelerator: 'Ctrl+W',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.close();
|
||||||
|
},
|
||||||
|
label: '&Close',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '&View',
|
||||||
|
submenu:
|
||||||
|
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
accelerator: 'Ctrl+R',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.reload();
|
||||||
|
},
|
||||||
|
label: '&Reload',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accelerator: 'F11',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.setFullScreen(
|
||||||
|
!this.mainWindow.isFullScreen(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: 'Toggle &Full Screen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accelerator: 'Alt+Ctrl+I',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.toggleDevTools();
|
||||||
|
},
|
||||||
|
label: 'Toggle &Developer Tools',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
accelerator: 'F11',
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.setFullScreen(
|
||||||
|
!this.mainWindow.isFullScreen(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: 'Toggle &Full Screen',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://electronjs.org');
|
||||||
|
},
|
||||||
|
label: 'Learn More',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal(
|
||||||
|
'https://github.com/electron/electron/tree/main/docs#readme',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://www.electronjs.org/community');
|
||||||
|
},
|
||||||
|
label: 'Community Discussions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
click() {
|
||||||
|
shell.openExternal('https://github.com/electron/electron/issues');
|
||||||
|
},
|
||||||
|
label: 'Search Issues',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return templateDefault;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,12 @@ import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player';
|
|||||||
import { utils } from './preload/utils';
|
import { utils } from './preload/utils';
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
browser,
|
browser,
|
||||||
ipc,
|
ipc,
|
||||||
localSettings,
|
localSettings,
|
||||||
lyrics,
|
lyrics,
|
||||||
mpris,
|
mpris,
|
||||||
mpvPlayer,
|
mpvPlayer,
|
||||||
mpvPlayerListener,
|
mpvPlayerListener,
|
||||||
utils,
|
utils,
|
||||||
});
|
});
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
const exit = () => {
|
const exit = () => {
|
||||||
ipcRenderer.send('window-close');
|
ipcRenderer.send('window-close');
|
||||||
};
|
};
|
||||||
const maximize = () => {
|
const maximize = () => {
|
||||||
ipcRenderer.send('window-maximize');
|
ipcRenderer.send('window-maximize');
|
||||||
};
|
};
|
||||||
const minimize = () => {
|
const minimize = () => {
|
||||||
ipcRenderer.send('window-minimize');
|
ipcRenderer.send('window-minimize');
|
||||||
};
|
};
|
||||||
const unmaximize = () => {
|
const unmaximize = () => {
|
||||||
ipcRenderer.send('window-unmaximize');
|
ipcRenderer.send('window-unmaximize');
|
||||||
};
|
};
|
||||||
|
|
||||||
const devtools = () => {
|
const devtools = () => {
|
||||||
ipcRenderer.send('window-dev-tools');
|
ipcRenderer.send('window-dev-tools');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const browser = {
|
export const browser = {
|
||||||
devtools,
|
devtools,
|
||||||
exit,
|
exit,
|
||||||
maximize,
|
maximize,
|
||||||
minimize,
|
minimize,
|
||||||
unmaximize,
|
unmaximize,
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
const removeAllListeners = (channel: string) => {
|
const removeAllListeners = (channel: string) => {
|
||||||
ipcRenderer.removeAllListeners(channel);
|
ipcRenderer.removeAllListeners(channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const send = (channel: string, ...args: any[]) => {
|
const send = (channel: string, ...args: any[]) => {
|
||||||
ipcRenderer.send(channel, ...args);
|
ipcRenderer.send(channel, ...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ipc = {
|
export const ipc = {
|
||||||
removeAllListeners,
|
removeAllListeners,
|
||||||
send,
|
send,
|
||||||
};
|
};
|
||||||
|
@ -4,49 +4,49 @@ import Store from 'electron-store';
|
|||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
const set = (property: string, value: string | Record<string, unknown> | boolean | string[]) => {
|
const set = (property: string, value: string | Record<string, unknown> | boolean | string[]) => {
|
||||||
store.set(`${property}`, value);
|
store.set(`${property}`, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const get = (property: string) => {
|
const get = (property: string) => {
|
||||||
return store.get(`${property}`);
|
return store.get(`${property}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const restart = () => {
|
const restart = () => {
|
||||||
ipcRenderer.send('app-restart');
|
ipcRenderer.send('app-restart');
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableMediaKeys = () => {
|
const enableMediaKeys = () => {
|
||||||
ipcRenderer.send('global-media-keys-enable');
|
ipcRenderer.send('global-media-keys-enable');
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableMediaKeys = () => {
|
const disableMediaKeys = () => {
|
||||||
ipcRenderer.send('global-media-keys-disable');
|
ipcRenderer.send('global-media-keys-disable');
|
||||||
};
|
};
|
||||||
|
|
||||||
const passwordGet = async (server: string): Promise<string | null> => {
|
const passwordGet = async (server: string): Promise<string | null> => {
|
||||||
return ipcRenderer.invoke('password-get', server);
|
return ipcRenderer.invoke('password-get', server);
|
||||||
};
|
};
|
||||||
|
|
||||||
const passwordRemove = (server: string) => {
|
const passwordRemove = (server: string) => {
|
||||||
ipcRenderer.send('password-remove', server);
|
ipcRenderer.send('password-remove', server);
|
||||||
};
|
};
|
||||||
|
|
||||||
const passwordSet = async (password: string, server: string): Promise<boolean> => {
|
const passwordSet = async (password: string, server: string): Promise<boolean> => {
|
||||||
return ipcRenderer.invoke('password-set', password, server);
|
return ipcRenderer.invoke('password-set', password, server);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setZoomFactor = (zoomFactor: number) => {
|
const setZoomFactor = (zoomFactor: number) => {
|
||||||
webFrame.setZoomFactor(zoomFactor / 100);
|
webFrame.setZoomFactor(zoomFactor / 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const localSettings = {
|
export const localSettings = {
|
||||||
disableMediaKeys,
|
disableMediaKeys,
|
||||||
enableMediaKeys,
|
enableMediaKeys,
|
||||||
get,
|
get,
|
||||||
passwordGet,
|
passwordGet,
|
||||||
passwordRemove,
|
passwordRemove,
|
||||||
passwordSet,
|
passwordSet,
|
||||||
restart,
|
restart,
|
||||||
set,
|
set,
|
||||||
setZoomFactor,
|
setZoomFactor,
|
||||||
};
|
};
|
||||||
|
@ -2,22 +2,22 @@ import { ipcRenderer } from 'electron';
|
|||||||
import { LyricSearchQuery, QueueSong } from '/@/renderer/api/types';
|
import { LyricSearchQuery, QueueSong } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const getRemoteLyricsBySong = (song: QueueSong) => {
|
const getRemoteLyricsBySong = (song: QueueSong) => {
|
||||||
const result = ipcRenderer.invoke('lyric-by-song', song);
|
const result = ipcRenderer.invoke('lyric-by-song', song);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchRemoteLyrics = (params: LyricSearchQuery) => {
|
const searchRemoteLyrics = (params: LyricSearchQuery) => {
|
||||||
const result = ipcRenderer.invoke('lyric-search', params);
|
const result = ipcRenderer.invoke('lyric-search', params);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRemoteLyricsByRemoteId = (id: string) => {
|
const getRemoteLyricsByRemoteId = (id: string) => {
|
||||||
const result = ipcRenderer.invoke('lyric-by-remote-id', id);
|
const result = ipcRenderer.invoke('lyric-by-remote-id', id);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lyrics = {
|
export const lyrics = {
|
||||||
getRemoteLyricsByRemoteId,
|
getRemoteLyricsByRemoteId,
|
||||||
getRemoteLyricsBySong,
|
getRemoteLyricsBySong,
|
||||||
searchRemoteLyrics,
|
searchRemoteLyrics,
|
||||||
};
|
};
|
||||||
|
@ -2,69 +2,69 @@ import { IpcRendererEvent, ipcRenderer } from 'electron';
|
|||||||
import { QueueSong } from '/@/renderer/api/types';
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const updateSong = (args: { currentTime: number; song: QueueSong }) => {
|
const updateSong = (args: { currentTime: number; song: QueueSong }) => {
|
||||||
ipcRenderer.send('mpris-update-song', args);
|
ipcRenderer.send('mpris-update-song', args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePosition = (timeSec: number) => {
|
const updatePosition = (timeSec: number) => {
|
||||||
ipcRenderer.send('mpris-update-position', timeSec);
|
ipcRenderer.send('mpris-update-position', timeSec);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSeek = (timeSec: number) => {
|
const updateSeek = (timeSec: number) => {
|
||||||
ipcRenderer.send('mpris-update-seek', timeSec);
|
ipcRenderer.send('mpris-update-seek', timeSec);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVolume = (volume: number) => {
|
const updateVolume = (volume: number) => {
|
||||||
ipcRenderer.send('mpris-update-volume', volume);
|
ipcRenderer.send('mpris-update-volume', volume);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRepeat = (repeat: string) => {
|
const updateRepeat = (repeat: string) => {
|
||||||
ipcRenderer.send('mpris-update-repeat', repeat);
|
ipcRenderer.send('mpris-update-repeat', repeat);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateShuffle = (shuffle: boolean) => {
|
const updateShuffle = (shuffle: boolean) => {
|
||||||
ipcRenderer.send('mpris-update-shuffle', shuffle);
|
ipcRenderer.send('mpris-update-shuffle', shuffle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRepeat = () => {
|
const toggleRepeat = () => {
|
||||||
ipcRenderer.send('mpris-toggle-repeat');
|
ipcRenderer.send('mpris-toggle-repeat');
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleShuffle = () => {
|
const toggleShuffle = () => {
|
||||||
ipcRenderer.send('mpris-toggle-shuffle');
|
ipcRenderer.send('mpris-toggle-shuffle');
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
||||||
ipcRenderer.on('mpris-request-position', cb);
|
ipcRenderer.on('mpris-request-position', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||||
ipcRenderer.on('mpris-request-seek', cb);
|
ipcRenderer.on('mpris-request-seek', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||||
ipcRenderer.on('mpris-request-volume', cb);
|
ipcRenderer.on('mpris-request-volume', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestToggleRepeat = (cb: (event: IpcRendererEvent) => void) => {
|
const requestToggleRepeat = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('mpris-request-toggle-repeat', cb);
|
ipcRenderer.on('mpris-request-toggle-repeat', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestToggleShuffle = (cb: (event: IpcRendererEvent) => void) => {
|
const requestToggleShuffle = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mpris = {
|
export const mpris = {
|
||||||
requestPosition,
|
requestPosition,
|
||||||
requestSeek,
|
requestSeek,
|
||||||
requestToggleRepeat,
|
requestToggleRepeat,
|
||||||
requestToggleShuffle,
|
requestToggleShuffle,
|
||||||
requestVolume,
|
requestVolume,
|
||||||
toggleRepeat,
|
toggleRepeat,
|
||||||
toggleShuffle,
|
toggleShuffle,
|
||||||
updatePosition,
|
updatePosition,
|
||||||
updateRepeat,
|
updateRepeat,
|
||||||
updateSeek,
|
updateSeek,
|
||||||
updateShuffle,
|
updateShuffle,
|
||||||
updateSong,
|
updateSong,
|
||||||
updateVolume,
|
updateVolume,
|
||||||
};
|
};
|
||||||
|
@ -2,213 +2,213 @@ import { ipcRenderer, IpcRendererEvent } from 'electron';
|
|||||||
import { PlayerData } from '/@/renderer/store';
|
import { PlayerData } from '/@/renderer/store';
|
||||||
|
|
||||||
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
ipcRenderer.send('player-initialize', data);
|
ipcRenderer.send('player-initialize', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const restart = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
const restart = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
ipcRenderer.send('player-restart', data);
|
ipcRenderer.send('player-restart', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRunning = () => {
|
const isRunning = () => {
|
||||||
return ipcRenderer.invoke('player-is-running');
|
return ipcRenderer.invoke('player-is-running');
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
return ipcRenderer.invoke('player-clean-up');
|
return ipcRenderer.invoke('player-clean-up');
|
||||||
};
|
};
|
||||||
|
|
||||||
const setProperties = (data: Record<string, any>) => {
|
const setProperties = (data: Record<string, any>) => {
|
||||||
console.log('Setting property :>>', data);
|
console.log('Setting property :>>', data);
|
||||||
ipcRenderer.send('player-set-properties', data);
|
ipcRenderer.send('player-set-properties', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const autoNext = (data: PlayerData) => {
|
const autoNext = (data: PlayerData) => {
|
||||||
ipcRenderer.send('player-auto-next', data);
|
ipcRenderer.send('player-auto-next', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentTime = () => {
|
const currentTime = () => {
|
||||||
ipcRenderer.send('player-current-time');
|
ipcRenderer.send('player-current-time');
|
||||||
};
|
};
|
||||||
|
|
||||||
const mute = () => {
|
const mute = () => {
|
||||||
ipcRenderer.send('player-mute');
|
ipcRenderer.send('player-mute');
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
ipcRenderer.send('player-next');
|
ipcRenderer.send('player-next');
|
||||||
};
|
};
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
ipcRenderer.send('player-pause');
|
ipcRenderer.send('player-pause');
|
||||||
};
|
};
|
||||||
|
|
||||||
const play = () => {
|
const play = () => {
|
||||||
ipcRenderer.send('player-play');
|
ipcRenderer.send('player-play');
|
||||||
};
|
};
|
||||||
|
|
||||||
const previous = () => {
|
const previous = () => {
|
||||||
ipcRenderer.send('player-previous');
|
ipcRenderer.send('player-previous');
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreQueue = () => {
|
const restoreQueue = () => {
|
||||||
ipcRenderer.send('player-restore-queue');
|
ipcRenderer.send('player-restore-queue');
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveQueue = (data: Record<string, any>) => {
|
const saveQueue = (data: Record<string, any>) => {
|
||||||
ipcRenderer.send('player-save-queue', data);
|
ipcRenderer.send('player-save-queue', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const seek = (seconds: number) => {
|
const seek = (seconds: number) => {
|
||||||
ipcRenderer.send('player-seek', seconds);
|
ipcRenderer.send('player-seek', seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const seekTo = (seconds: number) => {
|
const seekTo = (seconds: number) => {
|
||||||
ipcRenderer.send('player-seek-to', seconds);
|
ipcRenderer.send('player-seek-to', seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setQueue = (data: PlayerData, pause?: boolean) => {
|
const setQueue = (data: PlayerData, pause?: boolean) => {
|
||||||
ipcRenderer.send('player-set-queue', data, pause);
|
ipcRenderer.send('player-set-queue', data, pause);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setQueueNext = (data: PlayerData) => {
|
const setQueueNext = (data: PlayerData) => {
|
||||||
ipcRenderer.send('player-set-queue-next', data);
|
ipcRenderer.send('player-set-queue-next', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
ipcRenderer.send('player-stop');
|
ipcRenderer.send('player-stop');
|
||||||
};
|
};
|
||||||
|
|
||||||
const volume = (value: number) => {
|
const volume = (value: number) => {
|
||||||
ipcRenderer.send('player-volume', value);
|
ipcRenderer.send('player-volume', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const quit = () => {
|
const quit = () => {
|
||||||
ipcRenderer.send('player-quit');
|
ipcRenderer.send('player-quit');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentTime = async () => {
|
const getCurrentTime = async () => {
|
||||||
return ipcRenderer.invoke('player-get-time');
|
return ipcRenderer.invoke('player-get-time');
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-auto-next', cb);
|
ipcRenderer.on('renderer-player-auto-next', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererCurrentTime = (cb: (event: IpcRendererEvent, data: number) => void) => {
|
const rendererCurrentTime = (cb: (event: IpcRendererEvent, data: number) => void) => {
|
||||||
ipcRenderer.on('renderer-player-current-time', cb);
|
ipcRenderer.on('renderer-player-current-time', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-next', cb);
|
ipcRenderer.on('renderer-player-next', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-pause', cb);
|
ipcRenderer.on('renderer-player-pause', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPlay = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPlay = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-play', cb);
|
ipcRenderer.on('renderer-player-play', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPlayPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPlayPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-play-pause', cb);
|
ipcRenderer.on('renderer-player-play-pause', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPrevious = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPrevious = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-previous', cb);
|
ipcRenderer.on('renderer-player-previous', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererStop = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererStop = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-stop', cb);
|
ipcRenderer.on('renderer-player-stop', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererSkipForward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererSkipForward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-skip-forward', cb);
|
ipcRenderer.on('renderer-player-skip-forward', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererSkipBackward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererSkipBackward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-skip-backward', cb);
|
ipcRenderer.on('renderer-player-skip-backward', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererVolumeUp = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererVolumeUp = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-volume-up', cb);
|
ipcRenderer.on('renderer-player-volume-up', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererVolumeDown = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererVolumeDown = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-volume-down', cb);
|
ipcRenderer.on('renderer-player-volume-down', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererVolumeMute = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererVolumeMute = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-volume-mute', cb);
|
ipcRenderer.on('renderer-player-volume-mute', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererToggleRepeat = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererToggleRepeat = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-toggle-repeat', cb);
|
ipcRenderer.on('renderer-player-toggle-repeat', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererToggleShuffle = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererToggleShuffle = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-toggle-shuffle', cb);
|
ipcRenderer.on('renderer-player-toggle-shuffle', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('renderer-player-quit', cb);
|
ipcRenderer.on('renderer-player-quit', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('renderer-player-save-queue', cb);
|
ipcRenderer.on('renderer-player-save-queue', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererRestoreQueue = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererRestoreQueue = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('renderer-player-restore-queue', cb);
|
ipcRenderer.on('renderer-player-restore-queue', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
||||||
ipcRenderer.on('renderer-player-error', cb);
|
ipcRenderer.on('renderer-player-error', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mpvPlayer = {
|
export const mpvPlayer = {
|
||||||
autoNext,
|
autoNext,
|
||||||
cleanup,
|
cleanup,
|
||||||
currentTime,
|
currentTime,
|
||||||
getCurrentTime,
|
getCurrentTime,
|
||||||
initialize,
|
initialize,
|
||||||
isRunning,
|
isRunning,
|
||||||
mute,
|
mute,
|
||||||
next,
|
next,
|
||||||
pause,
|
pause,
|
||||||
play,
|
play,
|
||||||
previous,
|
previous,
|
||||||
quit,
|
quit,
|
||||||
restart,
|
restart,
|
||||||
restoreQueue,
|
restoreQueue,
|
||||||
saveQueue,
|
saveQueue,
|
||||||
seek,
|
seek,
|
||||||
seekTo,
|
seekTo,
|
||||||
setProperties,
|
setProperties,
|
||||||
setQueue,
|
setQueue,
|
||||||
setQueueNext,
|
setQueueNext,
|
||||||
stop,
|
stop,
|
||||||
volume,
|
volume,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mpvPlayerListener = {
|
export const mpvPlayerListener = {
|
||||||
rendererAutoNext,
|
rendererAutoNext,
|
||||||
rendererCurrentTime,
|
rendererCurrentTime,
|
||||||
rendererError,
|
rendererError,
|
||||||
rendererNext,
|
rendererNext,
|
||||||
rendererPause,
|
rendererPause,
|
||||||
rendererPlay,
|
rendererPlay,
|
||||||
rendererPlayPause,
|
rendererPlayPause,
|
||||||
rendererPrevious,
|
rendererPrevious,
|
||||||
rendererQuit,
|
rendererQuit,
|
||||||
rendererRestoreQueue,
|
rendererRestoreQueue,
|
||||||
rendererSaveQueue,
|
rendererSaveQueue,
|
||||||
rendererSkipBackward,
|
rendererSkipBackward,
|
||||||
rendererSkipForward,
|
rendererSkipForward,
|
||||||
rendererStop,
|
rendererStop,
|
||||||
rendererToggleRepeat,
|
rendererToggleRepeat,
|
||||||
rendererToggleShuffle,
|
rendererToggleShuffle,
|
||||||
rendererVolumeDown,
|
rendererVolumeDown,
|
||||||
rendererVolumeMute,
|
rendererVolumeMute,
|
||||||
rendererVolumeUp,
|
rendererVolumeUp,
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { isMacOS, isWindows, isLinux } from '../utils';
|
import { isMacOS, isWindows, isLinux } from '../utils';
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
isLinux,
|
isLinux,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
isWindows,
|
isWindows,
|
||||||
};
|
};
|
||||||
|
@ -6,47 +6,47 @@ import { URL } from 'url';
|
|||||||
export let resolveHtmlPath: (htmlFileName: string) => string;
|
export let resolveHtmlPath: (htmlFileName: string) => string;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
const port = process.env.PORT || 4343;
|
const port = process.env.PORT || 4343;
|
||||||
resolveHtmlPath = (htmlFileName: string) => {
|
resolveHtmlPath = (htmlFileName: string) => {
|
||||||
const url = new URL(`http://localhost:${port}`);
|
const url = new URL(`http://localhost:${port}`);
|
||||||
url.pathname = htmlFileName;
|
url.pathname = htmlFileName;
|
||||||
return url.href;
|
return url.href;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
resolveHtmlPath = (htmlFileName: string) => {
|
resolveHtmlPath = (htmlFileName: string) => {
|
||||||
return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
|
return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMacOS = () => {
|
export const isMacOS = () => {
|
||||||
return process.platform === 'darwin';
|
return process.platform === 'darwin';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isWindows = () => {
|
export const isWindows = () => {
|
||||||
return process.platform === 'win32';
|
return process.platform === 'win32';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLinux = () => {
|
export const isLinux = () => {
|
||||||
return process.platform === 'linux';
|
return process.platform === 'linux';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
||||||
let accelerator = hotkey;
|
let accelerator = hotkey;
|
||||||
|
|
||||||
const replacements = {
|
const replacements = {
|
||||||
mod: 'CmdOrCtrl',
|
mod: 'CmdOrCtrl',
|
||||||
numpad: 'num',
|
numpad: 'num',
|
||||||
numpadadd: 'numadd',
|
numpadadd: 'numadd',
|
||||||
numpaddecimal: 'numdec',
|
numpaddecimal: 'numdec',
|
||||||
numpaddivide: 'numdiv',
|
numpaddivide: 'numdiv',
|
||||||
numpadenter: 'numenter',
|
numpadenter: 'numenter',
|
||||||
numpadmultiply: 'nummult',
|
numpadmultiply: 'nummult',
|
||||||
numpadsubtract: 'numsub',
|
numpadsubtract: 'numsub',
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(replacements).forEach((key) => {
|
Object.keys(replacements).forEach((key) => {
|
||||||
accelerator = accelerator.replace(key, replacements[key as keyof typeof replacements]);
|
accelerator = accelerator.replace(key, replacements[key as keyof typeof replacements]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return accelerator;
|
return accelerator;
|
||||||
};
|
};
|
||||||
|
@ -1,53 +1,53 @@
|
|||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
import { toast } from '/@/renderer/components/toast/index';
|
||||||
import type {
|
import type {
|
||||||
AlbumDetailArgs,
|
AlbumDetailArgs,
|
||||||
AlbumListArgs,
|
AlbumListArgs,
|
||||||
SongListArgs,
|
SongListArgs,
|
||||||
SongDetailArgs,
|
SongDetailArgs,
|
||||||
AlbumArtistDetailArgs,
|
AlbumArtistDetailArgs,
|
||||||
AlbumArtistListArgs,
|
AlbumArtistListArgs,
|
||||||
SetRatingArgs,
|
SetRatingArgs,
|
||||||
GenreListArgs,
|
GenreListArgs,
|
||||||
CreatePlaylistArgs,
|
CreatePlaylistArgs,
|
||||||
DeletePlaylistArgs,
|
DeletePlaylistArgs,
|
||||||
PlaylistDetailArgs,
|
PlaylistDetailArgs,
|
||||||
PlaylistListArgs,
|
PlaylistListArgs,
|
||||||
MusicFolderListArgs,
|
MusicFolderListArgs,
|
||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
ArtistListArgs,
|
ArtistListArgs,
|
||||||
UpdatePlaylistArgs,
|
UpdatePlaylistArgs,
|
||||||
UserListArgs,
|
UserListArgs,
|
||||||
FavoriteArgs,
|
FavoriteArgs,
|
||||||
TopSongListArgs,
|
TopSongListArgs,
|
||||||
AddToPlaylistArgs,
|
AddToPlaylistArgs,
|
||||||
AddToPlaylistResponse,
|
AddToPlaylistResponse,
|
||||||
RemoveFromPlaylistArgs,
|
RemoveFromPlaylistArgs,
|
||||||
RemoveFromPlaylistResponse,
|
RemoveFromPlaylistResponse,
|
||||||
ScrobbleArgs,
|
ScrobbleArgs,
|
||||||
ScrobbleResponse,
|
ScrobbleResponse,
|
||||||
AlbumArtistDetailResponse,
|
AlbumArtistDetailResponse,
|
||||||
FavoriteResponse,
|
FavoriteResponse,
|
||||||
CreatePlaylistResponse,
|
CreatePlaylistResponse,
|
||||||
AlbumArtistListResponse,
|
AlbumArtistListResponse,
|
||||||
AlbumDetailResponse,
|
AlbumDetailResponse,
|
||||||
AlbumListResponse,
|
AlbumListResponse,
|
||||||
ArtistListResponse,
|
ArtistListResponse,
|
||||||
GenreListResponse,
|
GenreListResponse,
|
||||||
MusicFolderListResponse,
|
MusicFolderListResponse,
|
||||||
PlaylistDetailResponse,
|
PlaylistDetailResponse,
|
||||||
PlaylistListResponse,
|
PlaylistListResponse,
|
||||||
RatingResponse,
|
RatingResponse,
|
||||||
SongDetailResponse,
|
SongDetailResponse,
|
||||||
SongListResponse,
|
SongListResponse,
|
||||||
TopSongListResponse,
|
TopSongListResponse,
|
||||||
UpdatePlaylistResponse,
|
UpdatePlaylistResponse,
|
||||||
UserListResponse,
|
UserListResponse,
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
SearchArgs,
|
SearchArgs,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
LyricsArgs,
|
LyricsArgs,
|
||||||
LyricsResponse,
|
LyricsResponse,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { ServerType } from '/@/renderer/types';
|
import { ServerType } from '/@/renderer/types';
|
||||||
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
||||||
@ -56,436 +56,445 @@ import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
|
|||||||
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||||
|
|
||||||
export type ControllerEndpoint = Partial<{
|
export type ControllerEndpoint = Partial<{
|
||||||
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
|
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
|
||||||
authenticate: (
|
authenticate: (
|
||||||
url: string,
|
url: string,
|
||||||
body: { password: string; username: string },
|
body: { password: string; username: string },
|
||||||
) => Promise<AuthenticationResponse>;
|
) => Promise<AuthenticationResponse>;
|
||||||
clearPlaylist: () => void;
|
clearPlaylist: () => void;
|
||||||
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
||||||
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
|
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
|
||||||
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
||||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
||||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
||||||
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
||||||
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
|
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
|
||||||
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
||||||
getArtistDetail: () => void;
|
getArtistDetail: () => void;
|
||||||
getArtistInfo: (args: any) => void;
|
getArtistInfo: (args: any) => void;
|
||||||
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||||
getFavoritesList: () => void;
|
getFavoritesList: () => void;
|
||||||
getFolderItemList: () => void;
|
getFolderItemList: () => void;
|
||||||
getFolderList: () => void;
|
getFolderList: () => void;
|
||||||
getFolderSongs: () => void;
|
getFolderSongs: () => void;
|
||||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||||
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
|
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
|
||||||
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
||||||
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
||||||
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
||||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
||||||
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
||||||
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
||||||
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
||||||
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
||||||
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
|
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
|
||||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||||
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
||||||
search: (args: SearchArgs) => Promise<SearchResponse>;
|
search: (args: SearchArgs) => Promise<SearchResponse>;
|
||||||
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
|
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
|
||||||
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ApiController = {
|
type ApiController = {
|
||||||
jellyfin: ControllerEndpoint;
|
jellyfin: ControllerEndpoint;
|
||||||
navidrome: ControllerEndpoint;
|
navidrome: ControllerEndpoint;
|
||||||
subsonic: ControllerEndpoint;
|
subsonic: ControllerEndpoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoints: ApiController = {
|
const endpoints: ApiController = {
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
addToPlaylist: jfController.addToPlaylist,
|
addToPlaylist: jfController.addToPlaylist,
|
||||||
authenticate: jfController.authenticate,
|
authenticate: jfController.authenticate,
|
||||||
clearPlaylist: undefined,
|
clearPlaylist: undefined,
|
||||||
createFavorite: jfController.createFavorite,
|
createFavorite: jfController.createFavorite,
|
||||||
createPlaylist: jfController.createPlaylist,
|
createPlaylist: jfController.createPlaylist,
|
||||||
deleteFavorite: jfController.deleteFavorite,
|
deleteFavorite: jfController.deleteFavorite,
|
||||||
deletePlaylist: jfController.deletePlaylist,
|
deletePlaylist: jfController.deletePlaylist,
|
||||||
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
|
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
|
||||||
getAlbumArtistList: jfController.getAlbumArtistList,
|
getAlbumArtistList: jfController.getAlbumArtistList,
|
||||||
getAlbumDetail: jfController.getAlbumDetail,
|
getAlbumDetail: jfController.getAlbumDetail,
|
||||||
getAlbumList: jfController.getAlbumList,
|
getAlbumList: jfController.getAlbumList,
|
||||||
getArtistDetail: undefined,
|
getArtistDetail: undefined,
|
||||||
getArtistInfo: undefined,
|
getArtistInfo: undefined,
|
||||||
getArtistList: undefined,
|
getArtistList: undefined,
|
||||||
getFavoritesList: undefined,
|
getFavoritesList: undefined,
|
||||||
getFolderItemList: undefined,
|
getFolderItemList: undefined,
|
||||||
getFolderList: undefined,
|
getFolderList: undefined,
|
||||||
getFolderSongs: undefined,
|
getFolderSongs: undefined,
|
||||||
getGenreList: jfController.getGenreList,
|
getGenreList: jfController.getGenreList,
|
||||||
getLyrics: jfController.getLyrics,
|
getLyrics: jfController.getLyrics,
|
||||||
getMusicFolderList: jfController.getMusicFolderList,
|
getMusicFolderList: jfController.getMusicFolderList,
|
||||||
getPlaylistDetail: jfController.getPlaylistDetail,
|
getPlaylistDetail: jfController.getPlaylistDetail,
|
||||||
getPlaylistList: jfController.getPlaylistList,
|
getPlaylistList: jfController.getPlaylistList,
|
||||||
getPlaylistSongList: jfController.getPlaylistSongList,
|
getPlaylistSongList: jfController.getPlaylistSongList,
|
||||||
getRandomSongList: jfController.getRandomSongList,
|
getRandomSongList: jfController.getRandomSongList,
|
||||||
getSongDetail: undefined,
|
getSongDetail: undefined,
|
||||||
getSongList: jfController.getSongList,
|
getSongList: jfController.getSongList,
|
||||||
getTopSongs: jfController.getTopSongList,
|
getTopSongs: jfController.getTopSongList,
|
||||||
getUserList: undefined,
|
getUserList: undefined,
|
||||||
removeFromPlaylist: jfController.removeFromPlaylist,
|
removeFromPlaylist: jfController.removeFromPlaylist,
|
||||||
scrobble: jfController.scrobble,
|
scrobble: jfController.scrobble,
|
||||||
search: jfController.search,
|
search: jfController.search,
|
||||||
setRating: undefined,
|
setRating: undefined,
|
||||||
updatePlaylist: jfController.updatePlaylist,
|
updatePlaylist: jfController.updatePlaylist,
|
||||||
},
|
},
|
||||||
navidrome: {
|
navidrome: {
|
||||||
addToPlaylist: ndController.addToPlaylist,
|
addToPlaylist: ndController.addToPlaylist,
|
||||||
authenticate: ndController.authenticate,
|
authenticate: ndController.authenticate,
|
||||||
clearPlaylist: undefined,
|
clearPlaylist: undefined,
|
||||||
createFavorite: ssController.createFavorite,
|
createFavorite: ssController.createFavorite,
|
||||||
createPlaylist: ndController.createPlaylist,
|
createPlaylist: ndController.createPlaylist,
|
||||||
deleteFavorite: ssController.removeFavorite,
|
deleteFavorite: ssController.removeFavorite,
|
||||||
deletePlaylist: ndController.deletePlaylist,
|
deletePlaylist: ndController.deletePlaylist,
|
||||||
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
|
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
|
||||||
getAlbumArtistList: ndController.getAlbumArtistList,
|
getAlbumArtistList: ndController.getAlbumArtistList,
|
||||||
getAlbumDetail: ndController.getAlbumDetail,
|
getAlbumDetail: ndController.getAlbumDetail,
|
||||||
getAlbumList: ndController.getAlbumList,
|
getAlbumList: ndController.getAlbumList,
|
||||||
getArtistDetail: undefined,
|
getArtistDetail: undefined,
|
||||||
getArtistInfo: undefined,
|
getArtistInfo: undefined,
|
||||||
getArtistList: undefined,
|
getArtistList: undefined,
|
||||||
getFavoritesList: undefined,
|
getFavoritesList: undefined,
|
||||||
getFolderItemList: undefined,
|
getFolderItemList: undefined,
|
||||||
getFolderList: undefined,
|
getFolderList: undefined,
|
||||||
getFolderSongs: undefined,
|
getFolderSongs: undefined,
|
||||||
getGenreList: ndController.getGenreList,
|
getGenreList: ndController.getGenreList,
|
||||||
getLyrics: undefined,
|
getLyrics: undefined,
|
||||||
getMusicFolderList: ssController.getMusicFolderList,
|
getMusicFolderList: ssController.getMusicFolderList,
|
||||||
getPlaylistDetail: ndController.getPlaylistDetail,
|
getPlaylistDetail: ndController.getPlaylistDetail,
|
||||||
getPlaylistList: ndController.getPlaylistList,
|
getPlaylistList: ndController.getPlaylistList,
|
||||||
getPlaylistSongList: ndController.getPlaylistSongList,
|
getPlaylistSongList: ndController.getPlaylistSongList,
|
||||||
getRandomSongList: ssController.getRandomSongList,
|
getRandomSongList: ssController.getRandomSongList,
|
||||||
getSongDetail: ndController.getSongDetail,
|
getSongDetail: ndController.getSongDetail,
|
||||||
getSongList: ndController.getSongList,
|
getSongList: ndController.getSongList,
|
||||||
getTopSongs: ssController.getTopSongList,
|
getTopSongs: ssController.getTopSongList,
|
||||||
getUserList: ndController.getUserList,
|
getUserList: ndController.getUserList,
|
||||||
removeFromPlaylist: ndController.removeFromPlaylist,
|
removeFromPlaylist: ndController.removeFromPlaylist,
|
||||||
scrobble: ssController.scrobble,
|
scrobble: ssController.scrobble,
|
||||||
search: ssController.search3,
|
search: ssController.search3,
|
||||||
setRating: ssController.setRating,
|
setRating: ssController.setRating,
|
||||||
updatePlaylist: ndController.updatePlaylist,
|
updatePlaylist: ndController.updatePlaylist,
|
||||||
},
|
},
|
||||||
subsonic: {
|
subsonic: {
|
||||||
authenticate: ssController.authenticate,
|
authenticate: ssController.authenticate,
|
||||||
clearPlaylist: undefined,
|
clearPlaylist: undefined,
|
||||||
createFavorite: ssController.createFavorite,
|
createFavorite: ssController.createFavorite,
|
||||||
createPlaylist: undefined,
|
createPlaylist: undefined,
|
||||||
deleteFavorite: ssController.removeFavorite,
|
deleteFavorite: ssController.removeFavorite,
|
||||||
deletePlaylist: undefined,
|
deletePlaylist: undefined,
|
||||||
getAlbumArtistDetail: undefined,
|
getAlbumArtistDetail: undefined,
|
||||||
getAlbumArtistList: undefined,
|
getAlbumArtistList: undefined,
|
||||||
getAlbumDetail: undefined,
|
getAlbumDetail: undefined,
|
||||||
getAlbumList: undefined,
|
getAlbumList: undefined,
|
||||||
getArtistDetail: undefined,
|
getArtistDetail: undefined,
|
||||||
getArtistInfo: undefined,
|
getArtistInfo: undefined,
|
||||||
getArtistList: undefined,
|
getArtistList: undefined,
|
||||||
getFavoritesList: undefined,
|
getFavoritesList: undefined,
|
||||||
getFolderItemList: undefined,
|
getFolderItemList: undefined,
|
||||||
getFolderList: undefined,
|
getFolderList: undefined,
|
||||||
getFolderSongs: undefined,
|
getFolderSongs: undefined,
|
||||||
getGenreList: undefined,
|
getGenreList: undefined,
|
||||||
getLyrics: undefined,
|
getLyrics: undefined,
|
||||||
getMusicFolderList: ssController.getMusicFolderList,
|
getMusicFolderList: ssController.getMusicFolderList,
|
||||||
getPlaylistDetail: undefined,
|
getPlaylistDetail: undefined,
|
||||||
getPlaylistList: undefined,
|
getPlaylistList: undefined,
|
||||||
getSongDetail: undefined,
|
getSongDetail: undefined,
|
||||||
getSongList: undefined,
|
getSongList: undefined,
|
||||||
getTopSongs: ssController.getTopSongList,
|
getTopSongs: ssController.getTopSongList,
|
||||||
getUserList: undefined,
|
getUserList: undefined,
|
||||||
scrobble: ssController.scrobble,
|
scrobble: ssController.scrobble,
|
||||||
search: ssController.search3,
|
search: ssController.search3,
|
||||||
setRating: undefined,
|
setRating: undefined,
|
||||||
updatePlaylist: undefined,
|
updatePlaylist: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
|
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
|
||||||
const serverType = type || useAuthStore.getState().currentServer?.type;
|
const serverType = type || useAuthStore.getState().currentServer?.type;
|
||||||
|
|
||||||
if (!serverType) {
|
if (!serverType) {
|
||||||
toast.error({ message: 'No server selected', title: 'Unable to route request' });
|
toast.error({ message: 'No server selected', title: 'Unable to route request' });
|
||||||
throw new Error(`No server selected`);
|
throw new Error(`No server selected`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const controllerFn = endpoints?.[serverType]?.[endpoint];
|
const controllerFn = endpoints?.[serverType]?.[endpoint];
|
||||||
|
|
||||||
if (typeof controllerFn !== 'function') {
|
if (typeof controllerFn !== 'function') {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||||
title: 'Unable to route request',
|
title: 'Unable to route request',
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error(`Endpoint ${endpoint} is not implemented for ${serverType}`);
|
throw new Error(`Endpoint ${endpoint} is not implemented for ${serverType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints[serverType][endpoint];
|
return endpoints[serverType][endpoint];
|
||||||
};
|
};
|
||||||
|
|
||||||
const authenticate = async (
|
const authenticate = async (
|
||||||
url: string,
|
url: string,
|
||||||
body: { legacy?: boolean; password: string; username: string },
|
body: { legacy?: boolean; password: string; username: string },
|
||||||
type: ServerType,
|
type: ServerType,
|
||||||
) => {
|
) => {
|
||||||
return (apiController('authenticate', type) as ControllerEndpoint['authenticate'])?.(url, body);
|
return (apiController('authenticate', type) as ControllerEndpoint['authenticate'])?.(url, body);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumList = async (args: AlbumListArgs) => {
|
const getAlbumList = async (args: AlbumListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getAlbumList',
|
'getAlbumList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getAlbumList']
|
) as ControllerEndpoint['getAlbumList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumDetail = async (args: AlbumDetailArgs) => {
|
const getAlbumDetail = async (args: AlbumDetailArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getAlbumDetail',
|
'getAlbumDetail',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getAlbumDetail']
|
) as ControllerEndpoint['getAlbumDetail']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongList = async (args: SongListArgs) => {
|
const getSongList = async (args: SongListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getSongList',
|
'getSongList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getSongList']
|
) as ControllerEndpoint['getSongList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongDetail = async (args: SongDetailArgs) => {
|
const getSongDetail = async (args: SongDetailArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getSongDetail',
|
'getSongDetail',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getSongDetail']
|
) as ControllerEndpoint['getSongDetail']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMusicFolderList = async (args: MusicFolderListArgs) => {
|
const getMusicFolderList = async (args: MusicFolderListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getMusicFolderList',
|
'getMusicFolderList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getMusicFolderList']
|
) as ControllerEndpoint['getMusicFolderList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGenreList = async (args: GenreListArgs) => {
|
const getGenreList = async (args: GenreListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getGenreList',
|
'getGenreList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getGenreList']
|
) as ControllerEndpoint['getGenreList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs) => {
|
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getAlbumArtistDetail',
|
'getAlbumArtistDetail',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getAlbumArtistDetail']
|
) as ControllerEndpoint['getAlbumArtistDetail']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
|
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getAlbumArtistList',
|
'getAlbumArtistList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getAlbumArtistList']
|
) as ControllerEndpoint['getAlbumArtistList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getArtistList = async (args: ArtistListArgs) => {
|
const getArtistList = async (args: ArtistListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getArtistList',
|
'getArtistList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getArtistList']
|
) as ControllerEndpoint['getArtistList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistList = async (args: PlaylistListArgs) => {
|
const getPlaylistList = async (args: PlaylistListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getPlaylistList',
|
'getPlaylistList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getPlaylistList']
|
) as ControllerEndpoint['getPlaylistList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs) => {
|
const createPlaylist = async (args: CreatePlaylistArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'createPlaylist',
|
'createPlaylist',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['createPlaylist']
|
) as ControllerEndpoint['createPlaylist']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
|
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'updatePlaylist',
|
'updatePlaylist',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['updatePlaylist']
|
) as ControllerEndpoint['updatePlaylist']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePlaylist = async (args: DeletePlaylistArgs) => {
|
const deletePlaylist = async (args: DeletePlaylistArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'deletePlaylist',
|
'deletePlaylist',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['deletePlaylist']
|
) as ControllerEndpoint['deletePlaylist']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToPlaylist = async (args: AddToPlaylistArgs) => {
|
const addToPlaylist = async (args: AddToPlaylistArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'addToPlaylist',
|
'addToPlaylist',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['addToPlaylist']
|
) as ControllerEndpoint['addToPlaylist']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFromPlaylist = async (args: RemoveFromPlaylistArgs) => {
|
const removeFromPlaylist = async (args: RemoveFromPlaylistArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'removeFromPlaylist',
|
'removeFromPlaylist',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['removeFromPlaylist']
|
) as ControllerEndpoint['removeFromPlaylist']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistDetail = async (args: PlaylistDetailArgs) => {
|
const getPlaylistDetail = async (args: PlaylistDetailArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getPlaylistDetail',
|
'getPlaylistDetail',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getPlaylistDetail']
|
) as ControllerEndpoint['getPlaylistDetail']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistSongList = async (args: PlaylistSongListArgs) => {
|
const getPlaylistSongList = async (args: PlaylistSongListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getPlaylistSongList',
|
'getPlaylistSongList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getPlaylistSongList']
|
) as ControllerEndpoint['getPlaylistSongList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserList = async (args: UserListArgs) => {
|
const getUserList = async (args: UserListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getUserList',
|
'getUserList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getUserList']
|
) as ControllerEndpoint['getUserList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFavorite = async (args: FavoriteArgs) => {
|
const createFavorite = async (args: FavoriteArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'createFavorite',
|
'createFavorite',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['createFavorite']
|
) as ControllerEndpoint['createFavorite']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFavorite = async (args: FavoriteArgs) => {
|
const deleteFavorite = async (args: FavoriteArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'deleteFavorite',
|
'deleteFavorite',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['deleteFavorite']
|
) as ControllerEndpoint['deleteFavorite']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRating = async (args: SetRatingArgs) => {
|
const updateRating = async (args: SetRatingArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('setRating', args.apiClientProps.server?.type) as ControllerEndpoint['setRating']
|
apiController(
|
||||||
)?.(args);
|
'setRating',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['setRating']
|
||||||
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTopSongList = async (args: TopSongListArgs) => {
|
const getTopSongList = async (args: TopSongListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getTopSongs',
|
'getTopSongs',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getTopSongs']
|
) as ControllerEndpoint['getTopSongs']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrobble = async (args: ScrobbleArgs) => {
|
const scrobble = async (args: ScrobbleArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('scrobble', args.apiClientProps.server?.type) as ControllerEndpoint['scrobble']
|
apiController(
|
||||||
)?.(args);
|
'scrobble',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['scrobble']
|
||||||
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async (args: SearchArgs) => {
|
const search = async (args: SearchArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('search', args.apiClientProps.server?.type) as ControllerEndpoint['search']
|
apiController('search', args.apiClientProps.server?.type) as ControllerEndpoint['search']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomSongList = async (args: RandomSongListArgs) => {
|
const getRandomSongList = async (args: RandomSongListArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController(
|
apiController(
|
||||||
'getRandomSongList',
|
'getRandomSongList',
|
||||||
args.apiClientProps.server?.type,
|
args.apiClientProps.server?.type,
|
||||||
) as ControllerEndpoint['getRandomSongList']
|
) as ControllerEndpoint['getRandomSongList']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLyrics = async (args: LyricsArgs) => {
|
const getLyrics = async (args: LyricsArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('getLyrics', args.apiClientProps.server?.type) as ControllerEndpoint['getLyrics']
|
apiController(
|
||||||
)?.(args);
|
'getLyrics',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['getLyrics']
|
||||||
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const controller = {
|
export const controller = {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
authenticate,
|
authenticate,
|
||||||
createFavorite,
|
createFavorite,
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
deleteFavorite,
|
deleteFavorite,
|
||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
getAlbumArtistDetail,
|
getAlbumArtistDetail,
|
||||||
getAlbumArtistList,
|
getAlbumArtistList,
|
||||||
getAlbumDetail,
|
getAlbumDetail,
|
||||||
getAlbumList,
|
getAlbumList,
|
||||||
getArtistList,
|
getArtistList,
|
||||||
getGenreList,
|
getGenreList,
|
||||||
getLyrics,
|
getLyrics,
|
||||||
getMusicFolderList,
|
getMusicFolderList,
|
||||||
getPlaylistDetail,
|
getPlaylistDetail,
|
||||||
getPlaylistList,
|
getPlaylistList,
|
||||||
getPlaylistSongList,
|
getPlaylistSongList,
|
||||||
getRandomSongList,
|
getRandomSongList,
|
||||||
getSongDetail,
|
getSongDetail,
|
||||||
getSongList,
|
getSongList,
|
||||||
getTopSongList,
|
getTopSongList,
|
||||||
getUserList,
|
getUserList,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
scrobble,
|
scrobble,
|
||||||
search,
|
search,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
updateRating,
|
updateRating,
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
controller,
|
controller,
|
||||||
};
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -11,340 +11,340 @@ import { authenticationFailure } from '/@/renderer/api/utils';
|
|||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
export const contract = c.router({
|
export const contract = c.router({
|
||||||
addToPlaylist: {
|
addToPlaylist: {
|
||||||
body: z.null(),
|
body: z.null(),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: 'playlists/:id/items',
|
path: 'playlists/:id/items',
|
||||||
query: jfType._parameters.addToPlaylist,
|
query: jfType._parameters.addToPlaylist,
|
||||||
responses: {
|
responses: {
|
||||||
204: jfType._response.addToPlaylist,
|
204: jfType._response.addToPlaylist,
|
||||||
400: jfType._response.error,
|
400: jfType._response.error,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
authenticate: {
|
||||||
authenticate: {
|
body: jfType._parameters.authenticate,
|
||||||
body: jfType._parameters.authenticate,
|
headers: z.object({
|
||||||
headers: z.object({
|
'X-Emby-Authorization': z.string(),
|
||||||
'X-Emby-Authorization': z.string(),
|
}),
|
||||||
}),
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'users/authenticatebyname',
|
||||||
path: 'users/authenticatebyname',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.authenticate,
|
||||||
200: jfType._response.authenticate,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
createFavorite: {
|
||||||
createFavorite: {
|
body: jfType._parameters.favorite,
|
||||||
body: jfType._parameters.favorite,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'users/:userId/favoriteitems/:id',
|
||||||
path: 'users/:userId/favoriteitems/:id',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.favorite,
|
||||||
200: jfType._response.favorite,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
createPlaylist: {
|
||||||
createPlaylist: {
|
body: jfType._parameters.createPlaylist,
|
||||||
body: jfType._parameters.createPlaylist,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'playlists',
|
||||||
path: 'playlists',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.createPlaylist,
|
||||||
200: jfType._response.createPlaylist,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
deletePlaylist: {
|
||||||
deletePlaylist: {
|
body: null,
|
||||||
body: null,
|
method: 'DELETE',
|
||||||
method: 'DELETE',
|
path: 'items/:id',
|
||||||
path: 'items/:id',
|
responses: {
|
||||||
responses: {
|
204: jfType._response.deletePlaylist,
|
||||||
204: jfType._response.deletePlaylist,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumArtistDetail: {
|
||||||
getAlbumArtistDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items/:id',
|
||||||
path: 'users/:userId/items/:id',
|
query: jfType._parameters.albumArtistDetail,
|
||||||
query: jfType._parameters.albumArtistDetail,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.albumArtist,
|
||||||
200: jfType._response.albumArtist,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumArtistList: {
|
||||||
getAlbumArtistList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'artists/albumArtists',
|
||||||
path: 'artists/albumArtists',
|
query: jfType._parameters.albumArtistList,
|
||||||
query: jfType._parameters.albumArtistList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.albumArtistList,
|
||||||
200: jfType._response.albumArtistList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumDetail: {
|
||||||
getAlbumDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items/:id',
|
||||||
path: 'users/:userId/items/:id',
|
query: jfType._parameters.albumDetail,
|
||||||
query: jfType._parameters.albumDetail,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.album,
|
||||||
200: jfType._response.album,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumList: {
|
||||||
getAlbumList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items',
|
||||||
path: 'users/:userId/items',
|
query: jfType._parameters.albumList,
|
||||||
query: jfType._parameters.albumList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.albumList,
|
||||||
200: jfType._response.albumList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getArtistList: {
|
||||||
getArtistList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'artists',
|
||||||
path: 'artists',
|
query: jfType._parameters.albumArtistList,
|
||||||
query: jfType._parameters.albumArtistList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.albumArtistList,
|
||||||
200: jfType._response.albumArtistList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getGenreList: {
|
||||||
getGenreList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'genres',
|
||||||
path: 'genres',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.genreList,
|
||||||
200: jfType._response.genreList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getMusicFolderList: {
|
||||||
getMusicFolderList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items',
|
||||||
path: 'users/:userId/items',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.musicFolderList,
|
||||||
200: jfType._response.musicFolderList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getPlaylistDetail: {
|
||||||
getPlaylistDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items/:id',
|
||||||
path: 'users/:userId/items/:id',
|
query: jfType._parameters.playlistDetail,
|
||||||
query: jfType._parameters.playlistDetail,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.playlist,
|
||||||
200: jfType._response.playlist,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getPlaylistList: {
|
||||||
getPlaylistList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items',
|
||||||
path: 'users/:userId/items',
|
query: jfType._parameters.playlistList,
|
||||||
query: jfType._parameters.playlistList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.playlistList,
|
||||||
200: jfType._response.playlistList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getPlaylistSongList: {
|
||||||
getPlaylistSongList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'playlists/:id/items',
|
||||||
path: 'playlists/:id/items',
|
query: jfType._parameters.songList,
|
||||||
query: jfType._parameters.songList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.playlistSongList,
|
||||||
200: jfType._response.playlistSongList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getSimilarArtistList: {
|
||||||
getSimilarArtistList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'artists/:id/similar',
|
||||||
path: 'artists/:id/similar',
|
query: jfType._parameters.similarArtistList,
|
||||||
query: jfType._parameters.similarArtistList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.albumArtistList,
|
||||||
200: jfType._response.albumArtistList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getSongDetail: {
|
||||||
getSongDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'song/:id',
|
||||||
path: 'song/:id',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.song,
|
||||||
200: jfType._response.song,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getSongList: {
|
||||||
getSongList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items',
|
||||||
path: 'users/:userId/items',
|
query: jfType._parameters.songList,
|
||||||
query: jfType._parameters.songList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.songList,
|
||||||
200: jfType._response.songList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getSongLyrics: {
|
||||||
getSongLyrics: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/Items/:id/Lyrics',
|
||||||
path: 'users/:userId/Items/:id/Lyrics',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.lyrics,
|
||||||
200: jfType._response.lyrics,
|
404: jfType._response.error,
|
||||||
404: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
getTopSongsList: {
|
||||||
getTopSongsList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items',
|
||||||
path: 'users/:userId/items',
|
query: jfType._parameters.songList,
|
||||||
query: jfType._parameters.songList,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.topSongsList,
|
||||||
200: jfType._response.topSongsList,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
removeFavorite: {
|
||||||
removeFavorite: {
|
body: jfType._parameters.favorite,
|
||||||
body: jfType._parameters.favorite,
|
method: 'DELETE',
|
||||||
method: 'DELETE',
|
path: 'users/:userId/favoriteitems/:id',
|
||||||
path: 'users/:userId/favoriteitems/:id',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.favorite,
|
||||||
200: jfType._response.favorite,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
removeFromPlaylist: {
|
||||||
removeFromPlaylist: {
|
body: null,
|
||||||
body: null,
|
method: 'DELETE',
|
||||||
method: 'DELETE',
|
path: 'playlists/:id/items',
|
||||||
path: 'playlists/:id/items',
|
query: jfType._parameters.removeFromPlaylist,
|
||||||
query: jfType._parameters.removeFromPlaylist,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.removeFromPlaylist,
|
||||||
200: jfType._response.removeFromPlaylist,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
scrobblePlaying: {
|
||||||
scrobblePlaying: {
|
body: jfType._parameters.scrobble,
|
||||||
body: jfType._parameters.scrobble,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'sessions/playing',
|
||||||
path: 'sessions/playing',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.scrobble,
|
||||||
200: jfType._response.scrobble,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
scrobbleProgress: {
|
||||||
scrobbleProgress: {
|
body: jfType._parameters.scrobble,
|
||||||
body: jfType._parameters.scrobble,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'sessions/playing/progress',
|
||||||
path: 'sessions/playing/progress',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.scrobble,
|
||||||
200: jfType._response.scrobble,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
scrobbleStopped: {
|
||||||
scrobbleStopped: {
|
body: jfType._parameters.scrobble,
|
||||||
body: jfType._parameters.scrobble,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'sessions/playing/stopped',
|
||||||
path: 'sessions/playing/stopped',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.scrobble,
|
||||||
200: jfType._response.scrobble,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
search: {
|
||||||
search: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'users/:userId/items',
|
||||||
path: 'users/:userId/items',
|
query: jfType._parameters.search,
|
||||||
query: jfType._parameters.search,
|
responses: {
|
||||||
responses: {
|
200: jfType._response.search,
|
||||||
200: jfType._response.search,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
updatePlaylist: {
|
||||||
updatePlaylist: {
|
body: jfType._parameters.updatePlaylist,
|
||||||
body: jfType._parameters.updatePlaylist,
|
method: 'PUT',
|
||||||
method: 'PUT',
|
path: 'items/:id',
|
||||||
path: 'items/:id',
|
responses: {
|
||||||
responses: {
|
200: jfType._response.updatePlaylist,
|
||||||
200: jfType._response.updatePlaylist,
|
400: jfType._response.error,
|
||||||
400: jfType._response.error,
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosClient = axios.create({});
|
const axiosClient = axios.create({});
|
||||||
|
|
||||||
axiosClient.defaults.paramsSerializer = (params) => {
|
axiosClient.defaults.paramsSerializer = (params) => {
|
||||||
return qs.stringify(params, { arrayFormat: 'repeat' });
|
return qs.stringify(params, { arrayFormat: 'repeat' });
|
||||||
};
|
};
|
||||||
|
|
||||||
axiosClient.interceptors.response.use(
|
axiosClient.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
const currentServer = useAuthStore.getState().currentServer;
|
const currentServer = useAuthStore.getState().currentServer;
|
||||||
|
|
||||||
authenticationFailure(currentServer);
|
authenticationFailure(currentServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const parsePath = (fullPath: string) => {
|
const parsePath = (fullPath: string) => {
|
||||||
const [path, params] = fullPath.split('?');
|
const [path, params] = fullPath.split('?');
|
||||||
|
|
||||||
const parsedParams = qs.parse(params);
|
const parsedParams = qs.parse(params);
|
||||||
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: notNilParams,
|
params: notNilParams,
|
||||||
path,
|
path,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jfApiClient = (args: {
|
export const jfApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
url?: string;
|
url?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { server, url, signal } = args;
|
const { server, url, signal } = args;
|
||||||
|
|
||||||
return initClient(contract, {
|
return initClient(contract, {
|
||||||
api: async ({ path, method, headers, body }) => {
|
api: async ({ path, method, headers, body }) => {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
|
|
||||||
const { params, path: api } = parsePath(path);
|
const { params, path: api } = parsePath(path);
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
baseUrl = `${server?.url}`;
|
baseUrl = `${server?.url}`;
|
||||||
token = server?.credential;
|
token = server?.credential;
|
||||||
} else {
|
} else {
|
||||||
baseUrl = url;
|
baseUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await axiosClient.request({
|
const result = await axiosClient.request({
|
||||||
data: body,
|
data: body,
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
...(token && { 'X-MediaBrowser-Token': token }),
|
...(token && { 'X-MediaBrowser-Token': token }),
|
||||||
},
|
},
|
||||||
method: method as Method,
|
method: method as Method,
|
||||||
params,
|
params,
|
||||||
signal,
|
signal,
|
||||||
url: `${baseUrl}/${api}`,
|
url: `${baseUrl}/${api}`,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
body: result.data,
|
body: result.data,
|
||||||
headers: result.headers as any,
|
headers: result.headers as any,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
} catch (e: Error | AxiosError | any) {
|
} catch (e: Error | AxiosError | any) {
|
||||||
if (isAxiosError(e)) {
|
if (isAxiosError(e)) {
|
||||||
const error = e as AxiosError;
|
const error = e as AxiosError;
|
||||||
const response = error.response as AxiosResponse;
|
const response = error.response as AxiosResponse;
|
||||||
return {
|
return {
|
||||||
body: response?.data,
|
body: response?.data,
|
||||||
headers: response?.headers as any,
|
headers: response?.headers as any,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
baseHeaders: {
|
baseHeaders: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
jsonQuery: false,
|
jsonQuery: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,312 +3,316 @@ import { z } from 'zod';
|
|||||||
import { JFAlbum, JFPlaylist, JFMusicFolder, JFGenre } from '/@/renderer/api/jellyfin.types';
|
import { JFAlbum, JFPlaylist, JFMusicFolder, JFGenre } from '/@/renderer/api/jellyfin.types';
|
||||||
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
||||||
import {
|
import {
|
||||||
Song,
|
Song,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
Playlist,
|
Playlist,
|
||||||
MusicFolder,
|
MusicFolder,
|
||||||
Genre,
|
Genre,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||||
|
|
||||||
const getStreamUrl = (args: {
|
const getStreamUrl = (args: {
|
||||||
container?: string;
|
container?: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
eTag?: string;
|
eTag?: string;
|
||||||
id: string;
|
id: string;
|
||||||
mediaSourceId?: string;
|
mediaSourceId?: string;
|
||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { id, server, deviceId } = args;
|
const { id, server, deviceId } = args;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${server?.url}/audio` +
|
`${server?.url}/audio` +
|
||||||
`/${id}/universal` +
|
`/${id}/universal` +
|
||||||
`?userId=${server?.userId}` +
|
`?userId=${server?.userId}` +
|
||||||
`&deviceId=${deviceId}` +
|
`&deviceId=${deviceId}` +
|
||||||
'&audioCodec=aac' +
|
'&audioCodec=aac' +
|
||||||
`&api_key=${server?.credential}` +
|
`&api_key=${server?.credential}` +
|
||||||
`&playSessionId=${deviceId}` +
|
`&playSessionId=${deviceId}` +
|
||||||
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
|
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
|
||||||
'&transcodingContainer=ts' +
|
'&transcodingContainer=ts' +
|
||||||
'&transcodingProtocol=hls'
|
'&transcodingProtocol=hls'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumArtistCoverArtUrl = (args: {
|
const getAlbumArtistCoverArtUrl = (args: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
item: z.infer<typeof jfType._response.albumArtist>;
|
item: z.infer<typeof jfType._response.albumArtist>;
|
||||||
size: number;
|
size: number;
|
||||||
}) => {
|
}) => {
|
||||||
const size = args.size ? args.size : 300;
|
const size = args.size ? args.size : 300;
|
||||||
|
|
||||||
if (!args.item.ImageTags?.Primary) {
|
if (!args.item.ImageTags?.Primary) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/Items` +
|
`${args.baseUrl}/Items` +
|
||||||
`/${args.item.Id}` +
|
`/${args.item.Id}` +
|
||||||
'/Images/Primary' +
|
'/Images/Primary' +
|
||||||
`?width=${size}&height=${size}` +
|
`?width=${size}&height=${size}` +
|
||||||
'&quality=96'
|
'&quality=96'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => {
|
const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => {
|
||||||
const size = args.size ? args.size : 300;
|
const size = args.size ? args.size : 300;
|
||||||
|
|
||||||
if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) {
|
if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/Items` +
|
`${args.baseUrl}/Items` +
|
||||||
`/${args.item.Id}` +
|
`/${args.item.Id}` +
|
||||||
'/Images/Primary' +
|
'/Images/Primary' +
|
||||||
`?width=${size}&height=${size}` +
|
`?width=${size}&height=${size}` +
|
||||||
'&quality=96'
|
'&quality=96'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongCoverArtUrl = (args: {
|
const getSongCoverArtUrl = (args: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
item: z.infer<typeof jfType._response.song>;
|
item: z.infer<typeof jfType._response.song>;
|
||||||
size: number;
|
size: number;
|
||||||
}) => {
|
}) => {
|
||||||
const size = args.size ? args.size : 100;
|
const size = args.size ? args.size : 100;
|
||||||
|
|
||||||
if (args.item.ImageTags.Primary) {
|
if (args.item.ImageTags.Primary) {
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/Items` +
|
`${args.baseUrl}/Items` +
|
||||||
`/${args.item.Id}` +
|
`/${args.item.Id}` +
|
||||||
'/Images/Primary' +
|
'/Images/Primary' +
|
||||||
`?width=${size}&height=${size}` +
|
`?width=${size}&height=${size}` +
|
||||||
'&quality=96'
|
'&quality=96'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.item?.AlbumPrimaryImageTag) {
|
if (args.item?.AlbumPrimaryImageTag) {
|
||||||
// Fall back to album art if no image embedded
|
// Fall back to album art if no image embedded
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/Items` +
|
`${args.baseUrl}/Items` +
|
||||||
`/${args.item?.AlbumId}` +
|
`/${args.item?.AlbumId}` +
|
||||||
'/Images/Primary' +
|
'/Images/Primary' +
|
||||||
`?width=${size}&height=${size}` +
|
`?width=${size}&height=${size}` +
|
||||||
'&quality=96'
|
'&quality=96'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size: number }) => {
|
const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size: number }) => {
|
||||||
const size = args.size ? args.size : 300;
|
const size = args.size ? args.size : 300;
|
||||||
|
|
||||||
if (!args.item.ImageTags?.Primary) {
|
if (!args.item.ImageTags?.Primary) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/Items` +
|
`${args.baseUrl}/Items` +
|
||||||
`/${args.item.Id}` +
|
`/${args.item.Id}` +
|
||||||
'/Images/Primary' +
|
'/Images/Primary' +
|
||||||
`?width=${size}&height=${size}` +
|
`?width=${size}&height=${size}` +
|
||||||
'&quality=96'
|
'&quality=96'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof jfType._response.song>,
|
item: z.infer<typeof jfType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Song => {
|
): Song => {
|
||||||
return {
|
return {
|
||||||
album: item.Album,
|
album: item.Album,
|
||||||
albumArtists: item.AlbumArtists?.map((entry) => ({
|
albumArtists: item.AlbumArtists?.map((entry) => ({
|
||||||
id: entry.Id,
|
id: entry.Id,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: entry.Name,
|
name: entry.Name,
|
||||||
})),
|
})),
|
||||||
albumId: item.AlbumId,
|
albumId: item.AlbumId,
|
||||||
artistName: item?.ArtistItems?.[0]?.Name,
|
artistName: item?.ArtistItems?.[0]?.Name,
|
||||||
artists: item?.ArtistItems?.map((entry) => ({
|
artists: item?.ArtistItems?.map((entry) => ({
|
||||||
id: entry.Id,
|
id: entry.Id,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: entry.Name,
|
name: entry.Name,
|
||||||
})),
|
})),
|
||||||
bitRate: item.MediaSources && Number(Math.trunc(item.MediaSources[0]?.Bitrate / 1000)),
|
bitRate: item.MediaSources && Number(Math.trunc(item.MediaSources[0]?.Bitrate / 1000)),
|
||||||
bpm: null,
|
bpm: null,
|
||||||
channels: null,
|
channels: null,
|
||||||
comment: null,
|
comment: null,
|
||||||
compilation: null,
|
compilation: null,
|
||||||
container: (item.MediaSources && item.MediaSources[0]?.Container) || null,
|
container: (item.MediaSources && item.MediaSources[0]?.Container) || null,
|
||||||
createdAt: item.DateCreated,
|
createdAt: item.DateCreated,
|
||||||
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
|
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
|
||||||
duration: item.RunTimeTicks / 10000000,
|
duration: item.RunTimeTicks / 10000000,
|
||||||
genres: item.GenreItems.map((entry: any) => ({ id: entry.Id, name: entry.Name })),
|
genres: item.GenreItems.map((entry: any) => ({ id: entry.Id, name: entry.Name })),
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
imagePlaceholderUrl: null,
|
imagePlaceholderUrl: null,
|
||||||
imageUrl: getSongCoverArtUrl({ baseUrl: server?.url || '', item, size: imageSize || 100 }),
|
imageUrl: getSongCoverArtUrl({ baseUrl: server?.url || '', item, size: imageSize || 100 }),
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
lastPlayedAt: null,
|
lastPlayedAt: null,
|
||||||
lyrics: null,
|
lyrics: null,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
|
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
|
||||||
playCount: (item.UserData && item.UserData.PlayCount) || 0,
|
playCount: (item.UserData && item.UserData.PlayCount) || 0,
|
||||||
playlistItemId: item.PlaylistItemId,
|
playlistItemId: item.PlaylistItemId,
|
||||||
// releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null,
|
// releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null,
|
||||||
releaseDate: null,
|
releaseDate: null,
|
||||||
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
|
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
|
||||||
serverId: server?.id || '',
|
serverId: server?.id || '',
|
||||||
serverType: ServerType.JELLYFIN,
|
serverType: ServerType.JELLYFIN,
|
||||||
size: item.MediaSources && item.MediaSources[0]?.Size,
|
size: item.MediaSources && item.MediaSources[0]?.Size,
|
||||||
streamUrl: getStreamUrl({
|
streamUrl: getStreamUrl({
|
||||||
container: item.MediaSources?.[0]?.Container,
|
container: item.MediaSources?.[0]?.Container,
|
||||||
deviceId,
|
deviceId,
|
||||||
eTag: item.MediaSources?.[0]?.ETag,
|
eTag: item.MediaSources?.[0]?.ETag,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
mediaSourceId: item.MediaSources?.[0]?.Id,
|
mediaSourceId: item.MediaSources?.[0]?.Id,
|
||||||
server,
|
server,
|
||||||
}),
|
}),
|
||||||
trackNumber: item.IndexNumber,
|
trackNumber: item.IndexNumber,
|
||||||
uniqueId: nanoid(),
|
uniqueId: nanoid(),
|
||||||
updatedAt: item.DateCreated,
|
updatedAt: item.DateCreated,
|
||||||
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
|
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
|
||||||
userRating: null,
|
userRating: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof jfType._response.album>,
|
item: z.infer<typeof jfType._response.album>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
return {
|
return {
|
||||||
albumArtists:
|
albumArtists:
|
||||||
item.AlbumArtists.map((entry) => ({
|
item.AlbumArtists.map((entry) => ({
|
||||||
id: entry.Id,
|
id: entry.Id,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: entry.Name,
|
name: entry.Name,
|
||||||
})) || [],
|
})) || [],
|
||||||
artists: item.ArtistItems?.map((entry) => ({ id: entry.Id, imageUrl: null, name: entry.Name })),
|
artists: item.ArtistItems?.map((entry) => ({
|
||||||
backdropImageUrl: null,
|
id: entry.Id,
|
||||||
createdAt: item.DateCreated,
|
imageUrl: null,
|
||||||
duration: item.RunTimeTicks / 10000,
|
name: entry.Name,
|
||||||
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
})),
|
||||||
id: item.Id,
|
backdropImageUrl: null,
|
||||||
imagePlaceholderUrl: null,
|
createdAt: item.DateCreated,
|
||||||
imageUrl: getAlbumCoverArtUrl({
|
duration: item.RunTimeTicks / 10000,
|
||||||
baseUrl: server?.url || '',
|
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||||
item,
|
id: item.Id,
|
||||||
size: imageSize || 300,
|
imagePlaceholderUrl: null,
|
||||||
}),
|
imageUrl: getAlbumCoverArtUrl({
|
||||||
isCompilation: null,
|
baseUrl: server?.url || '',
|
||||||
itemType: LibraryItem.ALBUM,
|
item,
|
||||||
lastPlayedAt: null,
|
size: imageSize || 300,
|
||||||
name: item.Name,
|
}),
|
||||||
playCount: item.UserData?.PlayCount || 0,
|
isCompilation: null,
|
||||||
releaseDate: item.PremiereDate?.split('T')[0] || null,
|
itemType: LibraryItem.ALBUM,
|
||||||
releaseYear: item.ProductionYear || null,
|
lastPlayedAt: null,
|
||||||
serverId: server?.id || '',
|
name: item.Name,
|
||||||
serverType: ServerType.JELLYFIN,
|
playCount: item.UserData?.PlayCount || 0,
|
||||||
size: null,
|
releaseDate: item.PremiereDate?.split('T')[0] || null,
|
||||||
songCount: item?.ChildCount || null,
|
releaseYear: item.ProductionYear || null,
|
||||||
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
|
serverId: server?.id || '',
|
||||||
uniqueId: nanoid(),
|
serverType: ServerType.JELLYFIN,
|
||||||
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
size: null,
|
||||||
userFavorite: item.UserData?.IsFavorite || false,
|
songCount: item?.ChildCount || null,
|
||||||
userRating: null,
|
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
|
||||||
};
|
uniqueId: nanoid(),
|
||||||
|
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
||||||
|
userFavorite: item.UserData?.IsFavorite || false,
|
||||||
|
userRating: null,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbumArtist = (
|
const normalizeAlbumArtist = (
|
||||||
item: z.infer<typeof jfType._response.albumArtist> & {
|
item: z.infer<typeof jfType._response.albumArtist> & {
|
||||||
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
|
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
|
||||||
},
|
},
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const similarArtists =
|
const similarArtists =
|
||||||
item.similarArtists?.Items?.filter((entry) => entry.Name !== 'Various Artists').map(
|
item.similarArtists?.Items?.filter((entry) => entry.Name !== 'Various Artists').map(
|
||||||
(entry) => ({
|
(entry) => ({
|
||||||
id: entry.Id,
|
id: entry.Id,
|
||||||
imageUrl: getAlbumArtistCoverArtUrl({
|
imageUrl: getAlbumArtistCoverArtUrl({
|
||||||
baseUrl: server?.url || '',
|
baseUrl: server?.url || '',
|
||||||
item: entry,
|
item: entry,
|
||||||
size: imageSize || 300,
|
size: imageSize || 300,
|
||||||
}),
|
}),
|
||||||
name: entry.Name,
|
name: entry.Name,
|
||||||
}),
|
}),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: null,
|
albumCount: null,
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: item.Overview || null,
|
biography: item.Overview || null,
|
||||||
duration: item.RunTimeTicks / 10000,
|
duration: item.RunTimeTicks / 10000,
|
||||||
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
imageUrl: getAlbumArtistCoverArtUrl({
|
imageUrl: getAlbumArtistCoverArtUrl({
|
||||||
baseUrl: server?.url || '',
|
baseUrl: server?.url || '',
|
||||||
item,
|
item,
|
||||||
size: imageSize || 300,
|
size: imageSize || 300,
|
||||||
}),
|
}),
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
lastPlayedAt: null,
|
lastPlayedAt: null,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
playCount: item.UserData?.PlayCount || 0,
|
playCount: item.UserData?.PlayCount || 0,
|
||||||
serverId: server?.id || '',
|
serverId: server?.id || '',
|
||||||
serverType: ServerType.JELLYFIN,
|
serverType: ServerType.JELLYFIN,
|
||||||
similarArtists,
|
similarArtists,
|
||||||
songCount: null,
|
songCount: null,
|
||||||
userFavorite: item.UserData?.IsFavorite || false,
|
userFavorite: item.UserData?.IsFavorite || false,
|
||||||
userRating: null,
|
userRating: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizePlaylist = (
|
const normalizePlaylist = (
|
||||||
item: z.infer<typeof jfType._response.playlist>,
|
item: z.infer<typeof jfType._response.playlist>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Playlist => {
|
): Playlist => {
|
||||||
const imageUrl = getPlaylistCoverArtUrl({
|
const imageUrl = getPlaylistCoverArtUrl({
|
||||||
baseUrl: server?.url || '',
|
baseUrl: server?.url || '',
|
||||||
item,
|
item,
|
||||||
size: imageSize || 300,
|
size: imageSize || 300,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: item.Overview || null,
|
description: item.Overview || null,
|
||||||
duration: item.RunTimeTicks / 10000,
|
duration: item.RunTimeTicks / 10000,
|
||||||
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
imagePlaceholderUrl,
|
imagePlaceholderUrl,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: imageUrl || null,
|
||||||
itemType: LibraryItem.PLAYLIST,
|
itemType: LibraryItem.PLAYLIST,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
owner: null,
|
owner: null,
|
||||||
ownerId: null,
|
ownerId: null,
|
||||||
public: null,
|
public: null,
|
||||||
rules: null,
|
rules: null,
|
||||||
serverId: server?.id || '',
|
serverId: server?.id || '',
|
||||||
serverType: ServerType.JELLYFIN,
|
serverType: ServerType.JELLYFIN,
|
||||||
size: null,
|
size: null,
|
||||||
songCount: item?.ChildCount || null,
|
songCount: item?.ChildCount || null,
|
||||||
sync: null,
|
sync: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeMusicFolder = (item: JFMusicFolder): MusicFolder => {
|
const normalizeMusicFolder = (item: JFMusicFolder): MusicFolder => {
|
||||||
return {
|
return {
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// const normalizeArtist = (item: any) => {
|
// const normalizeArtist = (item: any) => {
|
||||||
@ -332,12 +336,12 @@ const normalizeMusicFolder = (item: JFMusicFolder): MusicFolder => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
const normalizeGenre = (item: JFGenre): Genre => {
|
const normalizeGenre = (item: JFGenre): Genre => {
|
||||||
return {
|
return {
|
||||||
albumCount: undefined,
|
albumCount: undefined,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
songCount: undefined,
|
songCount: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// const normalizeFolder = (item: any) => {
|
// const normalizeFolder = (item: any) => {
|
||||||
@ -360,10 +364,10 @@ const normalizeGenre = (item: JFGenre): Genre => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
export const jfNormalize = {
|
export const jfNormalize = {
|
||||||
album: normalizeAlbum,
|
album: normalizeAlbum,
|
||||||
albumArtist: normalizeAlbumArtist,
|
albumArtist: normalizeAlbumArtist,
|
||||||
genre: normalizeGenre,
|
genre: normalizeGenre,
|
||||||
musicFolder: normalizeMusicFolder,
|
musicFolder: normalizeMusicFolder,
|
||||||
playlist: normalizePlaylist,
|
playlist: normalizePlaylist,
|
||||||
song: normalizeSong,
|
song: normalizeSong,
|
||||||
};
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,143 +1,143 @@
|
|||||||
import { SSArtistInfo } from '/@/renderer/api/subsonic.types';
|
import { SSArtistInfo } from '/@/renderer/api/subsonic.types';
|
||||||
|
|
||||||
export type NDAuthenticate = {
|
export type NDAuthenticate = {
|
||||||
id: string;
|
id: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
subsonicSalt: string;
|
subsonicSalt: string;
|
||||||
subsonicToken: string;
|
subsonicToken: string;
|
||||||
token: string;
|
token: string;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDUser = {
|
export type NDUser = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
id: string;
|
id: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
lastAccessAt: string;
|
lastAccessAt: string;
|
||||||
lastLoginAt: string;
|
lastLoginAt: string;
|
||||||
name: string;
|
name: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDGenre = {
|
export type NDGenre = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAlbum = {
|
export type NDAlbum = {
|
||||||
albumArtist: string;
|
albumArtist: string;
|
||||||
albumArtistId: string;
|
albumArtistId: string;
|
||||||
allArtistIds: string;
|
allArtistIds: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
artistId: string;
|
artistId: string;
|
||||||
compilation: boolean;
|
compilation: boolean;
|
||||||
coverArtId?: string; // Removed after v0.48.0
|
coverArtId?: string; // Removed after v0.48.0
|
||||||
coverArtPath?: string; // Removed after v0.48.0
|
coverArtPath?: string; // Removed after v0.48.0
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
fullText: string;
|
fullText: string;
|
||||||
genre: string;
|
genre: string;
|
||||||
genres: NDGenre[];
|
genres: NDGenre[];
|
||||||
id: string;
|
id: string;
|
||||||
maxYear: number;
|
maxYear: number;
|
||||||
mbzAlbumArtistId: string;
|
mbzAlbumArtistId: string;
|
||||||
mbzAlbumId: string;
|
mbzAlbumId: string;
|
||||||
minYear: number;
|
minYear: number;
|
||||||
name: string;
|
name: string;
|
||||||
orderAlbumArtistName: string;
|
orderAlbumArtistName: string;
|
||||||
orderAlbumName: string;
|
orderAlbumName: string;
|
||||||
playCount: number;
|
playCount: number;
|
||||||
playDate: string;
|
playDate: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
size: number;
|
size: number;
|
||||||
songCount: number;
|
songCount: number;
|
||||||
sortAlbumArtistName: string;
|
sortAlbumArtistName: string;
|
||||||
sortArtistName: string;
|
sortArtistName: string;
|
||||||
starred: boolean;
|
starred: boolean;
|
||||||
starredAt: string;
|
starredAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
} & { songs?: NDSong[] };
|
} & { songs?: NDSong[] };
|
||||||
|
|
||||||
export type NDSong = {
|
export type NDSong = {
|
||||||
album: string;
|
album: string;
|
||||||
albumArtist: string;
|
albumArtist: string;
|
||||||
albumArtistId: string;
|
albumArtistId: string;
|
||||||
albumId: string;
|
albumId: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
artistId: string;
|
artistId: string;
|
||||||
bitRate: number;
|
bitRate: number;
|
||||||
bookmarkPosition: number;
|
bookmarkPosition: number;
|
||||||
bpm?: number;
|
bpm?: number;
|
||||||
channels?: number;
|
channels?: number;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
compilation: boolean;
|
compilation: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
discNumber: number;
|
discNumber: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
fullText: string;
|
fullText: string;
|
||||||
genre: string;
|
genre: string;
|
||||||
genres: NDGenre[];
|
genres: NDGenre[];
|
||||||
hasCoverArt: boolean;
|
hasCoverArt: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
lyrics?: string;
|
lyrics?: string;
|
||||||
mbzAlbumArtistId: string;
|
mbzAlbumArtistId: string;
|
||||||
mbzAlbumId: string;
|
mbzAlbumId: string;
|
||||||
mbzArtistId: string;
|
mbzArtistId: string;
|
||||||
mbzTrackId: string;
|
mbzTrackId: string;
|
||||||
orderAlbumArtistName: string;
|
orderAlbumArtistName: string;
|
||||||
orderAlbumName: string;
|
orderAlbumName: string;
|
||||||
orderArtistName: string;
|
orderArtistName: string;
|
||||||
orderTitle: string;
|
orderTitle: string;
|
||||||
path: string;
|
path: string;
|
||||||
playCount: number;
|
playCount: number;
|
||||||
playDate: string;
|
playDate: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
size: number;
|
size: number;
|
||||||
sortAlbumArtistName: string;
|
sortAlbumArtistName: string;
|
||||||
sortArtistName: string;
|
sortArtistName: string;
|
||||||
starred: boolean;
|
starred: boolean;
|
||||||
starredAt: string;
|
starredAt: string;
|
||||||
suffix: string;
|
suffix: string;
|
||||||
title: string;
|
title: string;
|
||||||
trackNumber: number;
|
trackNumber: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAlbumArtist = {
|
export type NDAlbumArtist = {
|
||||||
albumCount: number;
|
albumCount: number;
|
||||||
biography: string;
|
biography: string;
|
||||||
externalInfoUpdatedAt: string;
|
externalInfoUpdatedAt: string;
|
||||||
externalUrl: string;
|
externalUrl: string;
|
||||||
fullText: string;
|
fullText: string;
|
||||||
genres: NDGenre[];
|
genres: NDGenre[];
|
||||||
id: string;
|
id: string;
|
||||||
largeImageUrl?: string;
|
largeImageUrl?: string;
|
||||||
mbzArtistId: string;
|
mbzArtistId: string;
|
||||||
mediumImageUrl?: string;
|
mediumImageUrl?: string;
|
||||||
name: string;
|
name: string;
|
||||||
orderArtistName: string;
|
orderArtistName: string;
|
||||||
playCount: number;
|
playCount: number;
|
||||||
playDate: string;
|
playDate: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
size: number;
|
size: number;
|
||||||
smallImageUrl?: string;
|
smallImageUrl?: string;
|
||||||
songCount: number;
|
songCount: number;
|
||||||
starred: boolean;
|
starred: boolean;
|
||||||
starredAt: string;
|
starredAt: string;
|
||||||
} & {
|
} & {
|
||||||
similarArtists?: SSArtistInfo['similarArtist'];
|
similarArtists?: SSArtistInfo['similarArtist'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAuthenticationResponse = NDAuthenticate;
|
export type NDAuthenticationResponse = NDAuthenticate;
|
||||||
|
|
||||||
export type NDAlbumArtistList = {
|
export type NDAlbumArtistList = {
|
||||||
items: NDAlbumArtist[];
|
items: NDAlbumArtist[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAlbumArtistDetail = NDAlbumArtist;
|
export type NDAlbumArtistDetail = NDAlbumArtist;
|
||||||
@ -155,9 +155,9 @@ export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
|
|||||||
export type NDAlbumListResponse = NDAlbum[];
|
export type NDAlbumListResponse = NDAlbum[];
|
||||||
|
|
||||||
export type NDAlbumList = {
|
export type NDAlbumList = {
|
||||||
items: NDAlbum[];
|
items: NDAlbum[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDSongDetail = NDSong;
|
export type NDSongDetail = NDSong;
|
||||||
@ -167,142 +167,142 @@ export type NDSongDetailResponse = NDSong;
|
|||||||
export type NDSongListResponse = NDSong[];
|
export type NDSongListResponse = NDSong[];
|
||||||
|
|
||||||
export type NDSongList = {
|
export type NDSongList = {
|
||||||
items: NDSong[];
|
items: NDSong[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDArtistListResponse = NDAlbumArtist[];
|
export type NDArtistListResponse = NDAlbumArtist[];
|
||||||
|
|
||||||
export type NDPagination = {
|
export type NDPagination = {
|
||||||
_end?: number;
|
_end?: number;
|
||||||
_start?: number;
|
_start?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum NDSortOrder {
|
export enum NDSortOrder {
|
||||||
ASC = 'ASC',
|
ASC = 'ASC',
|
||||||
DESC = 'DESC',
|
DESC = 'DESC',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NDOrder = {
|
export type NDOrder = {
|
||||||
_order?: NDSortOrder;
|
_order?: NDSortOrder;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum NDGenreListSort {
|
export enum NDGenreListSort {
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NDGenreListParams = {
|
export type NDGenreListParams = {
|
||||||
_sort?: NDGenreListSort;
|
_sort?: NDGenreListSort;
|
||||||
id?: string;
|
id?: string;
|
||||||
} & NDPagination &
|
} & NDPagination &
|
||||||
NDOrder;
|
NDOrder;
|
||||||
|
|
||||||
export enum NDAlbumListSort {
|
export enum NDAlbumListSort {
|
||||||
ALBUM_ARTIST = 'albumArtist',
|
ALBUM_ARTIST = 'albumArtist',
|
||||||
ARTIST = 'artist',
|
ARTIST = 'artist',
|
||||||
DURATION = 'duration',
|
DURATION = 'duration',
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
PLAY_COUNT = 'playCount',
|
PLAY_COUNT = 'playCount',
|
||||||
PLAY_DATE = 'play_date',
|
PLAY_DATE = 'play_date',
|
||||||
RANDOM = 'random',
|
RANDOM = 'random',
|
||||||
RATING = 'rating',
|
RATING = 'rating',
|
||||||
RECENTLY_ADDED = 'recently_added',
|
RECENTLY_ADDED = 'recently_added',
|
||||||
SONG_COUNT = 'songCount',
|
SONG_COUNT = 'songCount',
|
||||||
STARRED = 'starred',
|
STARRED = 'starred',
|
||||||
YEAR = 'max_year',
|
YEAR = 'max_year',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NDAlbumListParams = {
|
export type NDAlbumListParams = {
|
||||||
_sort?: NDAlbumListSort;
|
_sort?: NDAlbumListSort;
|
||||||
album_id?: string;
|
album_id?: string;
|
||||||
artist_id?: string;
|
artist_id?: string;
|
||||||
compilation?: boolean;
|
compilation?: boolean;
|
||||||
genre_id?: string;
|
genre_id?: string;
|
||||||
has_rating?: boolean;
|
has_rating?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
recently_played?: boolean;
|
recently_played?: boolean;
|
||||||
starred?: boolean;
|
starred?: boolean;
|
||||||
year?: number;
|
year?: number;
|
||||||
} & NDPagination &
|
} & NDPagination &
|
||||||
NDOrder;
|
NDOrder;
|
||||||
|
|
||||||
export enum NDSongListSort {
|
export enum NDSongListSort {
|
||||||
ALBUM = 'album, order_album_artist_name, disc_number, track_number, title',
|
ALBUM = 'album, order_album_artist_name, disc_number, track_number, title',
|
||||||
ALBUM_ARTIST = 'order_album_artist_name, album, disc_number, track_number, title',
|
ALBUM_ARTIST = 'order_album_artist_name, album, disc_number, track_number, title',
|
||||||
ALBUM_SONGS = 'album, discNumber, trackNumber',
|
ALBUM_SONGS = 'album, discNumber, trackNumber',
|
||||||
ARTIST = 'artist',
|
ARTIST = 'artist',
|
||||||
BPM = 'bpm',
|
BPM = 'bpm',
|
||||||
CHANNELS = 'channels',
|
CHANNELS = 'channels',
|
||||||
COMMENT = 'comment',
|
COMMENT = 'comment',
|
||||||
DURATION = 'duration',
|
DURATION = 'duration',
|
||||||
FAVORITED = 'starred ASC, starredAt ASC',
|
FAVORITED = 'starred ASC, starredAt ASC',
|
||||||
GENRE = 'genre',
|
GENRE = 'genre',
|
||||||
ID = 'id',
|
ID = 'id',
|
||||||
PLAY_COUNT = 'playCount',
|
PLAY_COUNT = 'playCount',
|
||||||
PLAY_DATE = 'playDate',
|
PLAY_DATE = 'playDate',
|
||||||
RATING = 'rating',
|
RATING = 'rating',
|
||||||
RECENTLY_ADDED = 'createdAt',
|
RECENTLY_ADDED = 'createdAt',
|
||||||
TITLE = 'title',
|
TITLE = 'title',
|
||||||
TRACK = 'track',
|
TRACK = 'track',
|
||||||
YEAR = 'year, album, discNumber, trackNumber',
|
YEAR = 'year, album, discNumber, trackNumber',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NDSongListParams = {
|
export type NDSongListParams = {
|
||||||
_sort?: NDSongListSort;
|
_sort?: NDSongListSort;
|
||||||
album_id?: string[];
|
album_id?: string[];
|
||||||
artist_id?: string[];
|
artist_id?: string[];
|
||||||
genre_id?: string;
|
genre_id?: string;
|
||||||
starred?: boolean;
|
starred?: boolean;
|
||||||
} & NDPagination &
|
} & NDPagination &
|
||||||
NDOrder;
|
NDOrder;
|
||||||
|
|
||||||
export enum NDAlbumArtistListSort {
|
export enum NDAlbumArtistListSort {
|
||||||
ALBUM_COUNT = 'albumCount',
|
ALBUM_COUNT = 'albumCount',
|
||||||
FAVORITED = 'starred ASC, starredAt ASC',
|
FAVORITED = 'starred ASC, starredAt ASC',
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
PLAY_COUNT = 'playCount',
|
PLAY_COUNT = 'playCount',
|
||||||
RATING = 'rating',
|
RATING = 'rating',
|
||||||
SONG_COUNT = 'songCount',
|
SONG_COUNT = 'songCount',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NDAlbumArtistListParams = {
|
export type NDAlbumArtistListParams = {
|
||||||
_sort?: NDAlbumArtistListSort;
|
_sort?: NDAlbumArtistListSort;
|
||||||
genre_id?: string;
|
genre_id?: string;
|
||||||
starred?: boolean;
|
starred?: boolean;
|
||||||
} & NDPagination &
|
} & NDPagination &
|
||||||
NDOrder;
|
NDOrder;
|
||||||
|
|
||||||
export type NDAddToPlaylistResponse = {
|
export type NDAddToPlaylistResponse = {
|
||||||
added: number;
|
added: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAddToPlaylistBody = {
|
export type NDAddToPlaylistBody = {
|
||||||
ids: string[];
|
ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAddToPlaylist = null;
|
export type NDAddToPlaylist = null;
|
||||||
|
|
||||||
export type NDRemoveFromPlaylistResponse = {
|
export type NDRemoveFromPlaylistResponse = {
|
||||||
ids: string[];
|
ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDRemoveFromPlaylistParams = {
|
export type NDRemoveFromPlaylistParams = {
|
||||||
id: string[];
|
id: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDRemoveFromPlaylist = null;
|
export type NDRemoveFromPlaylist = null;
|
||||||
|
|
||||||
export type NDCreatePlaylistParams = {
|
export type NDCreatePlaylistParams = {
|
||||||
comment?: string;
|
comment?: string;
|
||||||
name: string;
|
name: string;
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
rules?: Record<string, any> | null;
|
rules?: Record<string, any> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDCreatePlaylistResponse = {
|
export type NDCreatePlaylistResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
||||||
@ -312,7 +312,7 @@ export type NDUpdatePlaylistParams = Partial<NDPlaylist>;
|
|||||||
export type NDUpdatePlaylistResponse = NDPlaylist;
|
export type NDUpdatePlaylistResponse = NDPlaylist;
|
||||||
|
|
||||||
export type NDDeletePlaylistParams = {
|
export type NDDeletePlaylistParams = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDDeletePlaylistResponse = null;
|
export type NDDeletePlaylistResponse = null;
|
||||||
@ -320,21 +320,21 @@ export type NDDeletePlaylistResponse = null;
|
|||||||
export type NDDeletePlaylist = NDDeletePlaylistResponse;
|
export type NDDeletePlaylist = NDDeletePlaylistResponse;
|
||||||
|
|
||||||
export type NDPlaylist = {
|
export type NDPlaylist = {
|
||||||
comment: string;
|
comment: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
evaluatedAt: string;
|
evaluatedAt: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
ownerName: string;
|
ownerName: string;
|
||||||
path: string;
|
path: string;
|
||||||
public: boolean;
|
public: boolean;
|
||||||
rules: Record<string, any> | null;
|
rules: Record<string, any> | null;
|
||||||
size: number;
|
size: number;
|
||||||
songCount: number;
|
songCount: number;
|
||||||
sync: boolean;
|
sync: boolean;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDPlaylistDetail = NDPlaylist;
|
export type NDPlaylistDetail = NDPlaylist;
|
||||||
@ -342,125 +342,125 @@ export type NDPlaylistDetail = NDPlaylist;
|
|||||||
export type NDPlaylistDetailResponse = NDPlaylist;
|
export type NDPlaylistDetailResponse = NDPlaylist;
|
||||||
|
|
||||||
export type NDPlaylistList = {
|
export type NDPlaylistList = {
|
||||||
items: NDPlaylist[];
|
items: NDPlaylist[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDPlaylistListResponse = NDPlaylist[];
|
export type NDPlaylistListResponse = NDPlaylist[];
|
||||||
|
|
||||||
export enum NDPlaylistListSort {
|
export enum NDPlaylistListSort {
|
||||||
DURATION = 'duration',
|
DURATION = 'duration',
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
OWNER = 'ownerName',
|
OWNER = 'ownerName',
|
||||||
PUBLIC = 'public',
|
PUBLIC = 'public',
|
||||||
SONG_COUNT = 'songCount',
|
SONG_COUNT = 'songCount',
|
||||||
UPDATED_AT = 'updatedAt',
|
UPDATED_AT = 'updatedAt',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NDPlaylistListParams = {
|
export type NDPlaylistListParams = {
|
||||||
_sort?: NDPlaylistListSort;
|
_sort?: NDPlaylistListSort;
|
||||||
owner_id?: string;
|
owner_id?: string;
|
||||||
} & NDPagination &
|
} & NDPagination &
|
||||||
NDOrder;
|
NDOrder;
|
||||||
|
|
||||||
export type NDPlaylistSong = NDSong & {
|
export type NDPlaylistSong = NDSong & {
|
||||||
mediaFileId: string;
|
mediaFileId: string;
|
||||||
playlistId: string;
|
playlistId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDPlaylistSongListResponse = NDPlaylistSong[];
|
export type NDPlaylistSongListResponse = NDPlaylistSong[];
|
||||||
|
|
||||||
export type NDPlaylistSongList = {
|
export type NDPlaylistSongList = {
|
||||||
items: NDPlaylistSong[];
|
items: NDPlaylistSong[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NDSongQueryFields = [
|
export const NDSongQueryFields = [
|
||||||
{ label: 'Album', type: 'string', value: 'album' },
|
{ label: 'Album', type: 'string', value: 'album' },
|
||||||
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
|
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
|
||||||
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
|
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
|
||||||
{ label: 'Album Type', type: 'string', value: 'albumtype' },
|
{ label: 'Album Type', type: 'string', value: 'albumtype' },
|
||||||
{ label: 'Artist', type: 'string', value: 'artist' },
|
{ label: 'Artist', type: 'string', value: 'artist' },
|
||||||
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
|
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
|
||||||
{ label: 'BPM', type: 'number', value: 'bpm' },
|
{ label: 'BPM', type: 'number', value: 'bpm' },
|
||||||
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
|
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
|
||||||
{ label: 'Channels', type: 'number', value: 'channels' },
|
{ label: 'Channels', type: 'number', value: 'channels' },
|
||||||
{ label: 'Comment', type: 'string', value: 'comment' },
|
{ label: 'Comment', type: 'string', value: 'comment' },
|
||||||
{ label: 'Date Added', type: 'date', value: 'dateadded' },
|
{ label: 'Date Added', type: 'date', value: 'dateadded' },
|
||||||
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
|
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
|
||||||
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
|
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
|
||||||
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
|
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
|
||||||
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
|
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
|
||||||
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
|
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
|
||||||
{ label: 'Duration', type: 'number', value: 'duration' },
|
{ label: 'Duration', type: 'number', value: 'duration' },
|
||||||
{ label: 'File Path', type: 'string', value: 'filepath' },
|
{ label: 'File Path', type: 'string', value: 'filepath' },
|
||||||
{ label: 'File Type', type: 'string', value: 'filetype' },
|
{ label: 'File Type', type: 'string', value: 'filetype' },
|
||||||
{ label: 'Genre', type: 'string', value: 'genre' },
|
{ label: 'Genre', type: 'string', value: 'genre' },
|
||||||
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
|
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
|
||||||
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
|
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
|
||||||
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
|
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
|
||||||
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
|
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
|
||||||
{ label: 'Name', type: 'string', value: 'title' },
|
{ label: 'Name', type: 'string', value: 'title' },
|
||||||
{ label: 'Play Count', type: 'number', value: 'playcount' },
|
{ label: 'Play Count', type: 'number', value: 'playcount' },
|
||||||
{ label: 'Rating', type: 'number', value: 'rating' },
|
{ label: 'Rating', type: 'number', value: 'rating' },
|
||||||
{ label: 'Size', type: 'number', value: 'size' },
|
{ label: 'Size', type: 'number', value: 'size' },
|
||||||
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
|
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
|
||||||
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
|
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
|
||||||
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
|
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
|
||||||
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
|
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
|
||||||
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
|
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
|
||||||
{ label: 'Year', type: 'number', value: 'year' },
|
{ label: 'Year', type: 'number', value: 'year' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NDSongQueryDateOperators = [
|
export const NDSongQueryDateOperators = [
|
||||||
{ label: 'is', value: 'is' },
|
{ label: 'is', value: 'is' },
|
||||||
{ label: 'is not', value: 'isNot' },
|
{ label: 'is not', value: 'isNot' },
|
||||||
{ label: 'is before', value: 'before' },
|
{ label: 'is before', value: 'before' },
|
||||||
{ label: 'is after', value: 'after' },
|
{ label: 'is after', value: 'after' },
|
||||||
{ label: 'is in the last', value: 'inTheLast' },
|
{ label: 'is in the last', value: 'inTheLast' },
|
||||||
{ label: 'is not in the last', value: 'notInTheLast' },
|
{ label: 'is not in the last', value: 'notInTheLast' },
|
||||||
{ label: 'is in the range', value: 'inTheRange' },
|
{ label: 'is in the range', value: 'inTheRange' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NDSongQueryStringOperators = [
|
export const NDSongQueryStringOperators = [
|
||||||
{ label: 'is', value: 'is' },
|
{ label: 'is', value: 'is' },
|
||||||
{ label: 'is not', value: 'isNot' },
|
{ label: 'is not', value: 'isNot' },
|
||||||
{ label: 'contains', value: 'contains' },
|
{ label: 'contains', value: 'contains' },
|
||||||
{ label: 'does not contain', value: 'notContains' },
|
{ label: 'does not contain', value: 'notContains' },
|
||||||
{ label: 'starts with', value: 'startsWith' },
|
{ label: 'starts with', value: 'startsWith' },
|
||||||
{ label: 'ends with', value: 'endsWith' },
|
{ label: 'ends with', value: 'endsWith' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NDSongQueryBooleanOperators = [
|
export const NDSongQueryBooleanOperators = [
|
||||||
{ label: 'is', value: 'is' },
|
{ label: 'is', value: 'is' },
|
||||||
{ label: 'is not', value: 'isNot' },
|
{ label: 'is not', value: 'isNot' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NDSongQueryNumberOperators = [
|
export const NDSongQueryNumberOperators = [
|
||||||
{ label: 'is', value: 'is' },
|
{ label: 'is', value: 'is' },
|
||||||
{ label: 'is not', value: 'isNot' },
|
{ label: 'is not', value: 'isNot' },
|
||||||
{ label: 'contains', value: 'contains' },
|
{ label: 'contains', value: 'contains' },
|
||||||
{ label: 'does not contain', value: 'notContains' },
|
{ label: 'does not contain', value: 'notContains' },
|
||||||
{ label: 'is greater than', value: 'gt' },
|
{ label: 'is greater than', value: 'gt' },
|
||||||
{ label: 'is less than', value: 'lt' },
|
{ label: 'is less than', value: 'lt' },
|
||||||
{ label: 'is in the range', value: 'inTheRange' },
|
{ label: 'is in the range', value: 'inTheRange' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type NDUserListParams = {
|
export type NDUserListParams = {
|
||||||
_sort?: NDUserListSort;
|
_sort?: NDUserListSort;
|
||||||
} & NDPagination &
|
} & NDPagination &
|
||||||
NDOrder;
|
NDOrder;
|
||||||
|
|
||||||
export type NDUserListResponse = NDUser[];
|
export type NDUserListResponse = NDUser[];
|
||||||
|
|
||||||
export type NDUserList = {
|
export type NDUserList = {
|
||||||
items: NDUser[];
|
items: NDUser[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum NDUserListSort {
|
export enum NDUserListSort {
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
}
|
}
|
||||||
|
@ -15,187 +15,188 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
|
|||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
export const contract = c.router({
|
export const contract = c.router({
|
||||||
addToPlaylist: {
|
addToPlaylist: {
|
||||||
body: ndType._parameters.addToPlaylist,
|
body: ndType._parameters.addToPlaylist,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: 'playlist/:id/tracks',
|
path: 'playlist/:id/tracks',
|
||||||
responses: {
|
responses: {
|
||||||
200: resultWithHeaders(ndType._response.addToPlaylist),
|
200: resultWithHeaders(ndType._response.addToPlaylist),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
authenticate: {
|
||||||
authenticate: {
|
body: ndType._parameters.authenticate,
|
||||||
body: ndType._parameters.authenticate,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'auth/login',
|
||||||
path: 'auth/login',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.authenticate),
|
||||||
200: resultWithHeaders(ndType._response.authenticate),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
createPlaylist: {
|
||||||
createPlaylist: {
|
body: ndType._parameters.createPlaylist,
|
||||||
body: ndType._parameters.createPlaylist,
|
method: 'POST',
|
||||||
method: 'POST',
|
path: 'playlist',
|
||||||
path: 'playlist',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.createPlaylist),
|
||||||
200: resultWithHeaders(ndType._response.createPlaylist),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
deletePlaylist: {
|
||||||
deletePlaylist: {
|
body: null,
|
||||||
body: null,
|
method: 'DELETE',
|
||||||
method: 'DELETE',
|
path: 'playlist/:id',
|
||||||
path: 'playlist/:id',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.deletePlaylist),
|
||||||
200: resultWithHeaders(ndType._response.deletePlaylist),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumArtistDetail: {
|
||||||
getAlbumArtistDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'artist/:id',
|
||||||
path: 'artist/:id',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.albumArtist),
|
||||||
200: resultWithHeaders(ndType._response.albumArtist),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumArtistList: {
|
||||||
getAlbumArtistList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'artist',
|
||||||
path: 'artist',
|
query: ndType._parameters.albumArtistList,
|
||||||
query: ndType._parameters.albumArtistList,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.albumArtistList),
|
||||||
200: resultWithHeaders(ndType._response.albumArtistList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumDetail: {
|
||||||
getAlbumDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'album/:id',
|
||||||
path: 'album/:id',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.album),
|
||||||
200: resultWithHeaders(ndType._response.album),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getAlbumList: {
|
||||||
getAlbumList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'album',
|
||||||
path: 'album',
|
query: ndType._parameters.albumList,
|
||||||
query: ndType._parameters.albumList,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.albumList),
|
||||||
200: resultWithHeaders(ndType._response.albumList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getGenreList: {
|
||||||
getGenreList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'genre',
|
||||||
path: 'genre',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.genreList),
|
||||||
200: resultWithHeaders(ndType._response.genreList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getPlaylistDetail: {
|
||||||
getPlaylistDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'playlist/:id',
|
||||||
path: 'playlist/:id',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.playlist),
|
||||||
200: resultWithHeaders(ndType._response.playlist),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getPlaylistList: {
|
||||||
getPlaylistList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'playlist',
|
||||||
path: 'playlist',
|
query: ndType._parameters.playlistList,
|
||||||
query: ndType._parameters.playlistList,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.playlistList),
|
||||||
200: resultWithHeaders(ndType._response.playlistList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getPlaylistSongList: {
|
||||||
getPlaylistSongList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'playlist/:id/tracks',
|
||||||
path: 'playlist/:id/tracks',
|
query: ndType._parameters.songList,
|
||||||
query: ndType._parameters.songList,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.playlistSongList),
|
||||||
200: resultWithHeaders(ndType._response.playlistSongList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getSongDetail: {
|
||||||
getSongDetail: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'song/:id',
|
||||||
path: 'song/:id',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.song),
|
||||||
200: resultWithHeaders(ndType._response.song),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getSongList: {
|
||||||
getSongList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'song',
|
||||||
path: 'song',
|
query: ndType._parameters.songList,
|
||||||
query: ndType._parameters.songList,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.songList),
|
||||||
200: resultWithHeaders(ndType._response.songList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
getUserList: {
|
||||||
getUserList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'user',
|
||||||
path: 'user',
|
query: ndType._parameters.userList,
|
||||||
query: ndType._parameters.userList,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.userList),
|
||||||
200: resultWithHeaders(ndType._response.userList),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
removeFromPlaylist: {
|
||||||
removeFromPlaylist: {
|
body: null,
|
||||||
body: null,
|
method: 'DELETE',
|
||||||
method: 'DELETE',
|
path: 'playlist/:id/tracks',
|
||||||
path: 'playlist/:id/tracks',
|
query: ndType._parameters.removeFromPlaylist,
|
||||||
query: ndType._parameters.removeFromPlaylist,
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.removeFromPlaylist),
|
||||||
200: resultWithHeaders(ndType._response.removeFromPlaylist),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
updatePlaylist: {
|
||||||
updatePlaylist: {
|
body: ndType._parameters.updatePlaylist,
|
||||||
body: ndType._parameters.updatePlaylist,
|
method: 'PUT',
|
||||||
method: 'PUT',
|
path: 'playlist/:id',
|
||||||
path: 'playlist/:id',
|
responses: {
|
||||||
responses: {
|
200: resultWithHeaders(ndType._response.updatePlaylist),
|
||||||
200: resultWithHeaders(ndType._response.updatePlaylist),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
500: resultWithHeaders(ndType._response.error),
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosClient = axios.create({});
|
const axiosClient = axios.create({});
|
||||||
|
|
||||||
axiosClient.defaults.paramsSerializer = (params) => {
|
axiosClient.defaults.paramsSerializer = (params) => {
|
||||||
return qs.stringify(params, { arrayFormat: 'repeat' });
|
return qs.stringify(params, { arrayFormat: 'repeat' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePath = (fullPath: string) => {
|
const parsePath = (fullPath: string) => {
|
||||||
const [path, params] = fullPath.split('?');
|
const [path, params] = fullPath.split('?');
|
||||||
|
|
||||||
const parsedParams = qs.parse(params);
|
const parsedParams = qs.parse(params);
|
||||||
|
|
||||||
// Convert indexed object to array
|
// Convert indexed object to array
|
||||||
const newParams: Record<string, any> = {};
|
const newParams: Record<string, any> = {};
|
||||||
Object.keys(parsedParams).forEach((key) => {
|
Object.keys(parsedParams).forEach((key) => {
|
||||||
const isIndexedArrayObject =
|
const isIndexedArrayObject =
|
||||||
typeof parsedParams[key] === 'object' && Object.keys(parsedParams[key] || {}).includes('0');
|
typeof parsedParams[key] === 'object' &&
|
||||||
|
Object.keys(parsedParams[key] || {}).includes('0');
|
||||||
|
|
||||||
if (!isIndexedArrayObject) {
|
if (!isIndexedArrayObject) {
|
||||||
newParams[key] = parsedParams[key];
|
newParams[key] = parsedParams[key];
|
||||||
} else {
|
} else {
|
||||||
newParams[key] = Object.values(parsedParams[key] || {});
|
newParams[key] = Object.values(parsedParams[key] || {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const notNilParams = omitBy(newParams, (value) => value === 'undefined' || value === 'null');
|
const notNilParams = omitBy(newParams, (value) => value === 'undefined' || value === 'null');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: notNilParams,
|
params: notNilParams,
|
||||||
path,
|
path,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let authSuccess = true;
|
let authSuccess = true;
|
||||||
@ -205,184 +206,186 @@ const RETRY_DELAY_MS = 1000;
|
|||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
|
|
||||||
const waitForResult = async (count = 0): Promise<void> => {
|
const waitForResult = async (count = 0): Promise<void> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (count === MAX_RETRIES || !shouldDelay) resolve();
|
if (count === MAX_RETRIES || !shouldDelay) resolve();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
waitForResult(count + 1)
|
waitForResult(count + 1)
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(resolve);
|
.catch(resolve);
|
||||||
}, RETRY_DELAY_MS);
|
}, RETRY_DELAY_MS);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const limitedFail = debounce(authenticationFailure, RETRY_DELAY_MS);
|
const limitedFail = debounce(authenticationFailure, RETRY_DELAY_MS);
|
||||||
const TIMEOUT_ERROR = Error();
|
const TIMEOUT_ERROR = Error();
|
||||||
|
|
||||||
axiosClient.interceptors.response.use(
|
axiosClient.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const serverId = useAuthStore.getState().currentServer?.id;
|
const serverId = useAuthStore.getState().currentServer?.id;
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const headerCredential = response.headers['x-nd-authorization'] as string | undefined;
|
const headerCredential = response.headers['x-nd-authorization'] as string | undefined;
|
||||||
|
|
||||||
if (headerCredential) {
|
if (headerCredential) {
|
||||||
useAuthStore.getState().actions.updateServer(serverId, {
|
useAuthStore.getState().actions.updateServer(serverId, {
|
||||||
ndCredential: headerCredential,
|
ndCredential: headerCredential,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authSuccess = true;
|
authSuccess = true;
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
const currentServer = useAuthStore.getState().currentServer;
|
const currentServer = useAuthStore.getState().currentServer;
|
||||||
|
|
||||||
if (localSettings && currentServer?.savePassword) {
|
if (localSettings && currentServer?.savePassword) {
|
||||||
// eslint-disable-next-line promise/no-promise-in-callback
|
// eslint-disable-next-line promise/no-promise-in-callback
|
||||||
return localSettings
|
return localSettings
|
||||||
.passwordGet(currentServer.id)
|
.passwordGet(currentServer.id)
|
||||||
.then(async (password: string | null) => {
|
.then(async (password: string | null) => {
|
||||||
authSuccess = false;
|
authSuccess = false;
|
||||||
|
|
||||||
if (password === null) {
|
if (password === null) {
|
||||||
throw error;
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDelay) {
|
||||||
|
await waitForResult();
|
||||||
|
|
||||||
|
// Hopefully the delay was sufficient for authentication.
|
||||||
|
// Otherwise, it will require manual intervention
|
||||||
|
if (authSuccess) {
|
||||||
|
return axiosClient.request(error.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDelay = true;
|
||||||
|
|
||||||
|
// Do not use axiosClient. Instead, manually make a post
|
||||||
|
const res = await axios.post(`${currentServer.url}/auth/login`, {
|
||||||
|
password,
|
||||||
|
username: currentServer.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 429) {
|
||||||
|
toast.error({
|
||||||
|
message:
|
||||||
|
'you have exceeded the number of allowed login requests. Please wait before logging, or consider tweaking AuthRequestLimit',
|
||||||
|
title: 'Your session has expired.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverId = currentServer.id;
|
||||||
|
useAuthStore
|
||||||
|
.getState()
|
||||||
|
.actions.updateServer(serverId, { ndCredential: undefined });
|
||||||
|
useAuthStore.getState().actions.setCurrentServer(null);
|
||||||
|
|
||||||
|
// special error to prevent sending a second message, and stop other messages that could be enqueued
|
||||||
|
limitedFail.cancel();
|
||||||
|
throw TIMEOUT_ERROR;
|
||||||
|
}
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to authenticate');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCredential = res.data.token;
|
||||||
|
const subsonicCredential = `u=${currentServer.username}&s=${res.data.subsonicSalt}&t=${res.data.subsonicToken}`;
|
||||||
|
|
||||||
|
useAuthStore.getState().actions.updateServer(currentServer.id, {
|
||||||
|
credential: subsonicCredential,
|
||||||
|
ndCredential: newCredential,
|
||||||
|
});
|
||||||
|
|
||||||
|
error.config.headers['x-nd-authorization'] = `Bearer ${newCredential}`;
|
||||||
|
|
||||||
|
authSuccess = true;
|
||||||
|
|
||||||
|
return axiosClient.request(error.config);
|
||||||
|
})
|
||||||
|
.catch((newError: any) => {
|
||||||
|
if (newError !== TIMEOUT_ERROR) {
|
||||||
|
console.error('Error when trying to reauthenticate: ', newError);
|
||||||
|
limitedFail(currentServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure to pass the error so axios will error later on
|
||||||
|
throw newError;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
shouldDelay = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDelay) {
|
limitedFail(currentServer);
|
||||||
await waitForResult();
|
}
|
||||||
|
|
||||||
// Hopefully the delay was sufficient for authentication.
|
return Promise.reject(error);
|
||||||
// Otherwise, it will require manual intervention
|
},
|
||||||
if (authSuccess) {
|
|
||||||
return axiosClient.request(error.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldDelay = true;
|
|
||||||
|
|
||||||
// Do not use axiosClient. Instead, manually make a post
|
|
||||||
const res = await axios.post(`${currentServer.url}/auth/login`, {
|
|
||||||
password,
|
|
||||||
username: currentServer.username,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 429) {
|
|
||||||
toast.error({
|
|
||||||
message:
|
|
||||||
'you have exceeded the number of allowed login requests. Please wait before logging, or consider tweaking AuthRequestLimit',
|
|
||||||
title: 'Your session has expired.',
|
|
||||||
});
|
|
||||||
|
|
||||||
const serverId = currentServer.id;
|
|
||||||
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
|
||||||
useAuthStore.getState().actions.setCurrentServer(null);
|
|
||||||
|
|
||||||
// special error to prevent sending a second message, and stop other messages that could be enqueued
|
|
||||||
limitedFail.cancel();
|
|
||||||
throw TIMEOUT_ERROR;
|
|
||||||
}
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error('Failed to authenticate');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCredential = res.data.token;
|
|
||||||
const subsonicCredential = `u=${currentServer.username}&s=${res.data.subsonicSalt}&t=${res.data.subsonicToken}`;
|
|
||||||
|
|
||||||
useAuthStore.getState().actions.updateServer(currentServer.id, {
|
|
||||||
credential: subsonicCredential,
|
|
||||||
ndCredential: newCredential,
|
|
||||||
});
|
|
||||||
|
|
||||||
error.config.headers['x-nd-authorization'] = `Bearer ${newCredential}`;
|
|
||||||
|
|
||||||
authSuccess = true;
|
|
||||||
|
|
||||||
return axiosClient.request(error.config);
|
|
||||||
})
|
|
||||||
.catch((newError: any) => {
|
|
||||||
if (newError !== TIMEOUT_ERROR) {
|
|
||||||
console.error('Error when trying to reauthenticate: ', newError);
|
|
||||||
limitedFail(currentServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to pass the error so axios will error later on
|
|
||||||
throw newError;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
shouldDelay = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
limitedFail(currentServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ndApiClient = (args: {
|
export const ndApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
url?: string;
|
url?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { server, url, signal } = args;
|
const { server, url, signal } = args;
|
||||||
|
|
||||||
return initClient(contract, {
|
return initClient(contract, {
|
||||||
api: async ({ path, method, headers, body }) => {
|
api: async ({ path, method, headers, body }) => {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
|
|
||||||
const { params, path: api } = parsePath(path);
|
const { params, path: api } = parsePath(path);
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
baseUrl = `${server?.url}/api`;
|
baseUrl = `${server?.url}/api`;
|
||||||
token = server?.ndCredential;
|
token = server?.ndCredential;
|
||||||
} else {
|
} else {
|
||||||
baseUrl = url;
|
baseUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (shouldDelay) await waitForResult();
|
if (shouldDelay) await waitForResult();
|
||||||
|
|
||||||
const result = await axiosClient.request({
|
const result = await axiosClient.request({
|
||||||
data: body,
|
data: body,
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
...(token && { 'x-nd-authorization': `Bearer ${token}` }),
|
...(token && { 'x-nd-authorization': `Bearer ${token}` }),
|
||||||
},
|
},
|
||||||
method: method as Method,
|
method: method as Method,
|
||||||
params,
|
params,
|
||||||
signal,
|
signal,
|
||||||
url: `${baseUrl}/${api}`,
|
url: `${baseUrl}/${api}`,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
body: { data: result.data, headers: result.headers },
|
body: { data: result.data, headers: result.headers },
|
||||||
headers: result.headers as any,
|
headers: result.headers as any,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
} catch (e: Error | AxiosError | any) {
|
} catch (e: Error | AxiosError | any) {
|
||||||
if (isAxiosError(e)) {
|
if (isAxiosError(e)) {
|
||||||
const error = e as AxiosError;
|
const error = e as AxiosError;
|
||||||
const response = error.response as AxiosResponse;
|
const response = error.response as AxiosResponse;
|
||||||
return {
|
return {
|
||||||
body: { data: response.data, headers: response.headers },
|
body: { data: response.data, headers: response.headers },
|
||||||
headers: response.headers as any,
|
headers: response.headers as any,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
baseHeaders: {
|
baseHeaders: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
jsonQuery: false,
|
jsonQuery: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,43 +1,43 @@
|
|||||||
import {
|
import {
|
||||||
AlbumArtistDetailArgs,
|
AlbumArtistDetailArgs,
|
||||||
AlbumArtistDetailResponse,
|
AlbumArtistDetailResponse,
|
||||||
AddToPlaylistArgs,
|
AddToPlaylistArgs,
|
||||||
AddToPlaylistResponse,
|
AddToPlaylistResponse,
|
||||||
CreatePlaylistResponse,
|
CreatePlaylistResponse,
|
||||||
CreatePlaylistArgs,
|
CreatePlaylistArgs,
|
||||||
DeletePlaylistArgs,
|
DeletePlaylistArgs,
|
||||||
DeletePlaylistResponse,
|
DeletePlaylistResponse,
|
||||||
AlbumArtistListResponse,
|
AlbumArtistListResponse,
|
||||||
AlbumArtistListArgs,
|
AlbumArtistListArgs,
|
||||||
albumArtistListSortMap,
|
albumArtistListSortMap,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
UserListResponse,
|
UserListResponse,
|
||||||
UserListArgs,
|
UserListArgs,
|
||||||
userListSortMap,
|
userListSortMap,
|
||||||
GenreListArgs,
|
GenreListArgs,
|
||||||
GenreListResponse,
|
GenreListResponse,
|
||||||
AlbumDetailResponse,
|
AlbumDetailResponse,
|
||||||
AlbumDetailArgs,
|
AlbumDetailArgs,
|
||||||
AlbumListArgs,
|
AlbumListArgs,
|
||||||
albumListSortMap,
|
albumListSortMap,
|
||||||
AlbumListResponse,
|
AlbumListResponse,
|
||||||
SongListResponse,
|
SongListResponse,
|
||||||
SongListArgs,
|
SongListArgs,
|
||||||
songListSortMap,
|
songListSortMap,
|
||||||
SongDetailResponse,
|
SongDetailResponse,
|
||||||
SongDetailArgs,
|
SongDetailArgs,
|
||||||
UpdatePlaylistArgs,
|
UpdatePlaylistArgs,
|
||||||
UpdatePlaylistResponse,
|
UpdatePlaylistResponse,
|
||||||
PlaylistListResponse,
|
PlaylistListResponse,
|
||||||
PlaylistDetailArgs,
|
PlaylistDetailArgs,
|
||||||
PlaylistListArgs,
|
PlaylistListArgs,
|
||||||
playlistListSortMap,
|
playlistListSortMap,
|
||||||
PlaylistDetailResponse,
|
PlaylistDetailResponse,
|
||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
RemoveFromPlaylistResponse,
|
RemoveFromPlaylistResponse,
|
||||||
RemoveFromPlaylistArgs,
|
RemoveFromPlaylistArgs,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||||
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||||
@ -45,428 +45,430 @@ import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
|||||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
|
|
||||||
const authenticate = async (
|
const authenticate = async (
|
||||||
url: string,
|
url: string,
|
||||||
body: { password: string; username: string },
|
body: { password: string; username: string },
|
||||||
): Promise<AuthenticationResponse> => {
|
): Promise<AuthenticationResponse> => {
|
||||||
const cleanServerUrl = url.replace(/\/$/, '');
|
const cleanServerUrl = url.replace(/\/$/, '');
|
||||||
|
|
||||||
const res = await ndApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
const res = await ndApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
||||||
body: {
|
body: {
|
||||||
password: body.password,
|
password: body.password,
|
||||||
username: body.username,
|
username: body.username,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to authenticate');
|
throw new Error('Failed to authenticate');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credential: `u=${body.username}&s=${res.body.data.subsonicSalt}&t=${res.body.data.subsonicToken}`,
|
credential: `u=${body.username}&s=${res.body.data.subsonicSalt}&t=${res.body.data.subsonicToken}`,
|
||||||
ndCredential: res.body.data.token,
|
ndCredential: res.body.data.token,
|
||||||
userId: res.body.data.id,
|
userId: res.body.data.id,
|
||||||
username: res.body.data.username,
|
username: res.body.data.username,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserList = async (args: UserListArgs): Promise<UserListResponse> => {
|
const getUserList = async (args: UserListArgs): Promise<UserListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getUserList({
|
const res = await ndApiClient(apiClientProps).getUserList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: userListSortMap.navidrome[query.sortBy],
|
_sort: userListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get user list');
|
throw new Error('Failed to get user list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((user) => ndNormalize.user(user)),
|
items: res.body.data.map((user) => ndNormalize.user(user)),
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: query?.startIndex || 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> => {
|
const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getGenreList({});
|
const res = await ndApiClient(apiClientProps).getGenreList({});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get genre list');
|
throw new Error('Failed to get genre list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data,
|
items: res.body.data,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumArtistDetail = async (
|
const getAlbumArtistDetail = async (
|
||||||
args: AlbumArtistDetailArgs,
|
args: AlbumArtistDetailArgs,
|
||||||
): Promise<AlbumArtistDetailResponse> => {
|
): Promise<AlbumArtistDetailResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
|
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
||||||
query: {
|
query: {
|
||||||
count: 10,
|
count: 10,
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get album artist detail');
|
throw new Error('Failed to get album artist detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiClientProps.server) {
|
if (!apiClientProps.server) {
|
||||||
throw new Error('Server is required');
|
throw new Error('Server is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ndNormalize.albumArtist(
|
return ndNormalize.albumArtist(
|
||||||
{
|
{
|
||||||
...res.body.data,
|
...res.body.data,
|
||||||
...(artistInfoRes.status === 200 && {
|
...(artistInfoRes.status === 200 && {
|
||||||
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
|
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
|
||||||
...(!res.body.data.largeImageUrl && {
|
...(!res.body.data.largeImageUrl && {
|
||||||
largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl,
|
largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl,
|
||||||
}),
|
}),
|
||||||
...(!res.body.data.mediumImageUrl && {
|
...(!res.body.data.mediumImageUrl && {
|
||||||
largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl,
|
largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl,
|
||||||
}),
|
}),
|
||||||
...(!res.body.data.smallImageUrl && {
|
...(!res.body.data.smallImageUrl && {
|
||||||
largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl,
|
largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtistListResponse> => {
|
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtistListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get album artist list');
|
throw new Error('Failed to get album artist list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((albumArtist) =>
|
items: res.body.data.map((albumArtist) =>
|
||||||
ndNormalize.albumArtist(albumArtist, apiClientProps.server),
|
ndNormalize.albumArtist(albumArtist, apiClientProps.server),
|
||||||
),
|
),
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<AlbumDetailResponse> => {
|
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<AlbumDetailResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
|
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const songsData = await ndApiClient(apiClientProps).getSongList({
|
const songsData = await ndApiClient(apiClientProps).getSongList({
|
||||||
query: {
|
query: {
|
||||||
_end: 0,
|
_end: 0,
|
||||||
_order: 'ASC',
|
_order: 'ASC',
|
||||||
_sort: 'album',
|
_sort: 'album',
|
||||||
_start: 0,
|
_start: 0,
|
||||||
album_id: [query.id],
|
album_id: [query.id],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (albumRes.status !== 200 || songsData.status !== 200) {
|
if (albumRes.status !== 200 || songsData.status !== 200) {
|
||||||
throw new Error('Failed to get album detail');
|
throw new Error('Failed to get album detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ndNormalize.album(
|
return ndNormalize.album(
|
||||||
{ ...albumRes.body.data, songs: songsData.body.data },
|
{ ...albumRes.body.data, songs: songsData.body.data },
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> => {
|
const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumList({
|
const res = await ndApiClient(apiClientProps).getAlbumList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: albumListSortMap.navidrome[query.sortBy],
|
_sort: albumListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
artist_id: query.artistIds?.[0],
|
artist_id: query.artistIds?.[0],
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get album list');
|
throw new Error('Failed to get album list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
|
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: query?.startIndex || 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
|
const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getSongList({
|
const res = await ndApiClient(apiClientProps).getSongList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || -1),
|
_end: query.startIndex + (query.limit || -1),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: songListSortMap.navidrome[query.sortBy],
|
_sort: songListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
album_artist_id: query.artistIds,
|
album_artist_id: query.artistIds,
|
||||||
album_id: query.albumIds,
|
album_id: query.albumIds,
|
||||||
title: query.searchTerm,
|
title: query.searchTerm,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get song list');
|
throw new Error('Failed to get song list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server, '')),
|
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server, '')),
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: query?.startIndex || 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse> => {
|
const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getSongDetail({
|
const res = await ndApiClient(apiClientProps).getSongDetail({
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get song detail');
|
throw new Error('Failed to get song detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ndNormalize.song(res.body.data, apiClientProps.server, '');
|
return ndNormalize.song(res.body.data, apiClientProps.server, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||||
const { body, apiClientProps } = args;
|
const { body, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).createPlaylist({
|
const res = await ndApiClient(apiClientProps).createPlaylist({
|
||||||
body: {
|
body: {
|
||||||
comment: body.comment,
|
comment: body.comment,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
public: body._custom?.navidrome?.public,
|
public: body._custom?.navidrome?.public,
|
||||||
rules: body._custom?.navidrome?.rules,
|
rules: body._custom?.navidrome?.rules,
|
||||||
sync: body._custom?.navidrome?.sync,
|
sync: body._custom?.navidrome?.sync,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to create playlist');
|
throw new Error('Failed to create playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: res.body.data.id,
|
id: res.body.data.id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
|
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
|
||||||
const { query, body, apiClientProps } = args;
|
const { query, body, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).updatePlaylist({
|
const res = await ndApiClient(apiClientProps).updatePlaylist({
|
||||||
body: {
|
body: {
|
||||||
comment: body.comment || '',
|
comment: body.comment || '',
|
||||||
name: body.name,
|
name: body.name,
|
||||||
public: body._custom?.navidrome?.public || false,
|
public: body._custom?.navidrome?.public || false,
|
||||||
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
|
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
|
||||||
sync: body._custom?.navidrome?.sync || undefined,
|
sync: body._custom?.navidrome?.sync || undefined,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to update playlist');
|
throw new Error('Failed to update playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistResponse> => {
|
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
||||||
body: null,
|
body: null,
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to delete playlist');
|
throw new Error('Failed to delete playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => {
|
const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getPlaylistList({
|
const res = await ndApiClient(apiClientProps).getPlaylistList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get playlist list');
|
throw new Error('Failed to get playlist list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)),
|
items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)),
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: query?.startIndex || 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<PlaylistDetailResponse> => {
|
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<PlaylistDetailResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
|
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get playlist detail');
|
throw new Error('Failed to get playlist detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ndNormalize.playlist(res.body.data, apiClientProps.server);
|
return ndNormalize.playlist(res.body.data, apiClientProps.server);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaylistSongList = async (
|
const getPlaylistSongList = async (
|
||||||
args: PlaylistSongListArgs,
|
args: PlaylistSongListArgs,
|
||||||
): Promise<PlaylistSongListResponse> => {
|
): Promise<PlaylistSongListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
|
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
||||||
_sort: query.sortBy ? songListSortMap.navidrome[query.sortBy] : ndType._enum.songList.ID,
|
_sort: query.sortBy
|
||||||
_start: query.startIndex,
|
? songListSortMap.navidrome[query.sortBy]
|
||||||
},
|
: ndType._enum.songList.ID,
|
||||||
});
|
_start: query.startIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get playlist song list');
|
throw new Error('Failed to get playlist song list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
|
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: query?.startIndex || 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToPlaylist = async (args: AddToPlaylistArgs): Promise<AddToPlaylistResponse> => {
|
const addToPlaylist = async (args: AddToPlaylistArgs): Promise<AddToPlaylistResponse> => {
|
||||||
const { body, query, apiClientProps } = args;
|
const { body, query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).addToPlaylist({
|
const res = await ndApiClient(apiClientProps).addToPlaylist({
|
||||||
body: {
|
body: {
|
||||||
ids: body.songId,
|
ids: body.songId,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to add to playlist');
|
throw new Error('Failed to add to playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFromPlaylist = async (
|
const removeFromPlaylist = async (
|
||||||
args: RemoveFromPlaylistArgs,
|
args: RemoveFromPlaylistArgs,
|
||||||
): Promise<RemoveFromPlaylistResponse> => {
|
): Promise<RemoveFromPlaylistResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||||
body: null,
|
body: null,
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
id: query.songId,
|
id: query.songId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to remove from playlist');
|
throw new Error('Failed to remove from playlist');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ndController = {
|
export const ndController = {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
authenticate,
|
authenticate,
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
getAlbumArtistDetail,
|
getAlbumArtistDetail,
|
||||||
getAlbumArtistList,
|
getAlbumArtistList,
|
||||||
getAlbumDetail,
|
getAlbumDetail,
|
||||||
getAlbumList,
|
getAlbumList,
|
||||||
getGenreList,
|
getGenreList,
|
||||||
getPlaylistDetail,
|
getPlaylistDetail,
|
||||||
getPlaylistList,
|
getPlaylistList,
|
||||||
getPlaylistSongList,
|
getPlaylistSongList,
|
||||||
getSongDetail,
|
getSongDetail,
|
||||||
getSongList,
|
getSongList,
|
||||||
getUserList,
|
getUserList,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
};
|
};
|
||||||
|
@ -6,226 +6,226 @@ import { ndType } from './navidrome-types';
|
|||||||
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
const getCoverArtUrl = (args: {
|
||||||
baseUrl: string | undefined;
|
baseUrl: string | undefined;
|
||||||
coverArtId: string;
|
coverArtId: string;
|
||||||
credential: string | undefined;
|
credential: string | undefined;
|
||||||
size: number;
|
size: number;
|
||||||
}) => {
|
}) => {
|
||||||
const size = args.size ? args.size : 250;
|
const size = args.size ? args.size : 250;
|
||||||
|
|
||||||
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/rest/getCoverArt.view` +
|
`${args.baseUrl}/rest/getCoverArt.view` +
|
||||||
`?id=${args.coverArtId}` +
|
`?id=${args.coverArtId}` +
|
||||||
`&${args.credential}` +
|
`&${args.credential}` +
|
||||||
'&v=1.13.0' +
|
'&v=1.13.0' +
|
||||||
'&c=feishin' +
|
'&c=feishin' +
|
||||||
`&size=${size}`
|
`&size=${size}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
|
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Song => {
|
): Song => {
|
||||||
let id;
|
let id;
|
||||||
let playlistItemId;
|
let playlistItemId;
|
||||||
|
|
||||||
// Dynamically determine the id field based on whether or not the item is a playlist song
|
// Dynamically determine the id field based on whether or not the item is a playlist song
|
||||||
if ('mediaFileId' in item) {
|
if ('mediaFileId' in item) {
|
||||||
id = item.mediaFileId;
|
id = item.mediaFileId;
|
||||||
playlistItemId = item.id;
|
playlistItemId = item.id;
|
||||||
} else {
|
} else {
|
||||||
id = item.id;
|
id = item.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = getCoverArtUrl({
|
const imageUrl = getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: id,
|
coverArtId: id,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: imageSize || 100,
|
size: imageSize || 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
album: item.album,
|
album: item.album,
|
||||||
albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
||||||
albumId: item.albumId,
|
albumId: item.albumId,
|
||||||
artistName: item.artist,
|
artistName: item.artist,
|
||||||
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
||||||
bitRate: item.bitRate,
|
bitRate: item.bitRate,
|
||||||
bpm: item.bpm ? item.bpm : null,
|
bpm: item.bpm ? item.bpm : null,
|
||||||
channels: item.channels ? item.channels : null,
|
channels: item.channels ? item.channels : null,
|
||||||
comment: item.comment ? item.comment : null,
|
comment: item.comment ? item.comment : null,
|
||||||
compilation: item.compilation,
|
compilation: item.compilation,
|
||||||
container: item.suffix,
|
container: item.suffix,
|
||||||
createdAt: item.createdAt.split('T')[0],
|
createdAt: item.createdAt.split('T')[0],
|
||||||
discNumber: item.discNumber,
|
discNumber: item.discNumber,
|
||||||
duration: item.duration,
|
duration: item.duration,
|
||||||
genres: item.genres,
|
genres: item.genres,
|
||||||
id,
|
id,
|
||||||
imagePlaceholderUrl,
|
imagePlaceholderUrl,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
||||||
lyrics: item.lyrics ? item.lyrics : null,
|
lyrics: item.lyrics ? item.lyrics : null,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
playCount: item.playCount,
|
playCount: item.playCount,
|
||||||
playlistItemId,
|
playlistItemId,
|
||||||
releaseDate: new Date(item.year, 0, 1).toISOString(),
|
releaseDate: new Date(item.year, 0, 1).toISOString(),
|
||||||
releaseYear: String(item.year),
|
releaseYear: String(item.year),
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.NAVIDROME,
|
serverType: ServerType.NAVIDROME,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`,
|
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`,
|
||||||
trackNumber: item.trackNumber,
|
trackNumber: item.trackNumber,
|
||||||
uniqueId: nanoid(),
|
uniqueId: nanoid(),
|
||||||
updatedAt: item.updatedAt,
|
updatedAt: item.updatedAt,
|
||||||
userFavorite: item.starred || false,
|
userFavorite: item.starred || false,
|
||||||
userRating: item.rating || null,
|
userRating: item.rating || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof ndType._response.album> & {
|
item: z.infer<typeof ndType._response.album> & {
|
||||||
songs?: z.infer<typeof ndType._response.songList>;
|
songs?: z.infer<typeof ndType._response.songList>;
|
||||||
},
|
},
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
const imageUrl = getCoverArtUrl({
|
const imageUrl = getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArtId || item.id,
|
coverArtId: item.coverArtId || item.id,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: imageSize || 300,
|
size: imageSize || 300,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
|
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
|
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
|
||||||
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
||||||
backdropImageUrl: imageBackdropUrl,
|
backdropImageUrl: imageBackdropUrl,
|
||||||
createdAt: item.createdAt.split('T')[0],
|
createdAt: item.createdAt.split('T')[0],
|
||||||
duration: item.duration * 1000 || null,
|
duration: item.duration * 1000 || null,
|
||||||
genres: item.genres,
|
genres: item.genres,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imagePlaceholderUrl,
|
imagePlaceholderUrl,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
isCompilation: item.compilation,
|
isCompilation: item.compilation,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
playCount: item.playCount,
|
playCount: item.playCount,
|
||||||
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
|
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
|
||||||
releaseYear: item.minYear,
|
releaseYear: item.minYear,
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.NAVIDROME,
|
serverType: ServerType.NAVIDROME,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
songCount: item.songCount,
|
songCount: item.songCount,
|
||||||
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined,
|
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined,
|
||||||
uniqueId: nanoid(),
|
uniqueId: nanoid(),
|
||||||
updatedAt: item.updatedAt,
|
updatedAt: item.updatedAt,
|
||||||
userFavorite: item.starred,
|
userFavorite: item.starred,
|
||||||
userRating: item.rating || null,
|
userRating: item.rating || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbumArtist = (
|
const normalizeAlbumArtist = (
|
||||||
item: z.infer<typeof ndType._response.albumArtist> & {
|
item: z.infer<typeof ndType._response.albumArtist> & {
|
||||||
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
|
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
|
||||||
},
|
},
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
item.largeImageUrl === '/app/artist-placeholder.webp' ? null : item.largeImageUrl;
|
item.largeImageUrl === '/app/artist-placeholder.webp' ? null : item.largeImageUrl;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: item.albumCount,
|
albumCount: item.albumCount,
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: item.biography || null,
|
biography: item.biography || null,
|
||||||
duration: null,
|
duration: null,
|
||||||
genres: item.genres,
|
genres: item.genres,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: imageUrl || null,
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
playCount: item.playCount,
|
playCount: item.playCount,
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.NAVIDROME,
|
serverType: ServerType.NAVIDROME,
|
||||||
similarArtists:
|
similarArtists:
|
||||||
item.similarArtists?.map((artist) => ({
|
item.similarArtists?.map((artist) => ({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
imageUrl: artist?.artistImageUrl || null,
|
imageUrl: artist?.artistImageUrl || null,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
})) || null,
|
})) || null,
|
||||||
songCount: item.songCount,
|
songCount: item.songCount,
|
||||||
userFavorite: item.starred,
|
userFavorite: item.starred,
|
||||||
userRating: item.rating,
|
userRating: item.rating,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizePlaylist = (
|
const normalizePlaylist = (
|
||||||
item: z.infer<typeof ndType._response.playlist>,
|
item: z.infer<typeof ndType._response.playlist>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Playlist => {
|
): Playlist => {
|
||||||
const imageUrl = getCoverArtUrl({
|
const imageUrl = getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.id,
|
coverArtId: item.id,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: imageSize || 300,
|
size: imageSize || 300,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: item.comment,
|
description: item.comment,
|
||||||
duration: item.duration * 1000,
|
duration: item.duration * 1000,
|
||||||
genres: [],
|
genres: [],
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imagePlaceholderUrl,
|
imagePlaceholderUrl,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
itemType: LibraryItem.PLAYLIST,
|
itemType: LibraryItem.PLAYLIST,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
owner: item.ownerName,
|
owner: item.ownerName,
|
||||||
ownerId: item.ownerId,
|
ownerId: item.ownerId,
|
||||||
public: item.public,
|
public: item.public,
|
||||||
rules: item?.rules || null,
|
rules: item?.rules || null,
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.NAVIDROME,
|
serverType: ServerType.NAVIDROME,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
songCount: item.songCount,
|
songCount: item.songCount,
|
||||||
sync: item.sync,
|
sync: item.sync,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
|
const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
|
||||||
return {
|
return {
|
||||||
createdAt: item.createdAt,
|
createdAt: item.createdAt,
|
||||||
email: item.email || null,
|
email: item.email || null,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
isAdmin: item.isAdmin,
|
isAdmin: item.isAdmin,
|
||||||
lastLoginAt: item.lastLoginAt,
|
lastLoginAt: item.lastLoginAt,
|
||||||
name: item.userName,
|
name: item.userName,
|
||||||
updatedAt: item.updatedAt,
|
updatedAt: item.updatedAt,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ndNormalize = {
|
export const ndNormalize = {
|
||||||
album: normalizeAlbum,
|
album: normalizeAlbum,
|
||||||
albumArtist: normalizeAlbumArtist,
|
albumArtist: normalizeAlbumArtist,
|
||||||
playlist: normalizePlaylist,
|
playlist: normalizePlaylist,
|
||||||
song: normalizeSong,
|
song: normalizeSong,
|
||||||
user: normalizeUser,
|
user: normalizeUser,
|
||||||
};
|
};
|
||||||
|
@ -5,294 +5,294 @@ const sortOrderValues = ['ASC', 'DESC'] as const;
|
|||||||
const error = z.string();
|
const error = z.string();
|
||||||
|
|
||||||
const paginationParameters = z.object({
|
const paginationParameters = z.object({
|
||||||
_end: z.number().optional(),
|
_end: z.number().optional(),
|
||||||
_order: z.enum(sortOrderValues),
|
_order: z.enum(sortOrderValues),
|
||||||
_start: z.number().optional(),
|
_start: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authenticate = z.object({
|
const authenticate = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isAdmin: z.boolean(),
|
isAdmin: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
subsonicSalt: z.string(),
|
subsonicSalt: z.string(),
|
||||||
subsonicToken: z.string(),
|
subsonicToken: z.string(),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authenticateParameters = z.object({
|
const authenticateParameters = z.object({
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = z.object({
|
const user = z.object({
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isAdmin: z.boolean(),
|
isAdmin: z.boolean(),
|
||||||
lastAccessAt: z.string(),
|
lastAccessAt: z.string(),
|
||||||
lastLoginAt: z.string(),
|
lastLoginAt: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
userName: z.string(),
|
userName: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const userList = z.array(user);
|
const userList = z.array(user);
|
||||||
|
|
||||||
const ndUserListSort = {
|
const ndUserListSort = {
|
||||||
NAME: 'name',
|
NAME: 'name',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const userListParameters = paginationParameters.extend({
|
const userListParameters = paginationParameters.extend({
|
||||||
_sort: z.nativeEnum(ndUserListSort).optional(),
|
_sort: z.nativeEnum(ndUserListSort).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const genre = z.object({
|
const genre = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const genreList = z.array(genre);
|
const genreList = z.array(genre);
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
albumCount: z.number(),
|
albumCount: z.number(),
|
||||||
biography: z.string(),
|
biography: z.string(),
|
||||||
externalInfoUpdatedAt: z.string(),
|
externalInfoUpdatedAt: z.string(),
|
||||||
externalUrl: z.string(),
|
externalUrl: z.string(),
|
||||||
fullText: z.string(),
|
fullText: z.string(),
|
||||||
genres: z.array(genre),
|
genres: z.array(genre),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
largeImageUrl: z.string().optional(),
|
largeImageUrl: z.string().optional(),
|
||||||
mbzArtistId: z.string().optional(),
|
mbzArtistId: z.string().optional(),
|
||||||
mediumImageUrl: z.string().optional(),
|
mediumImageUrl: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
orderArtistName: z.string(),
|
orderArtistName: z.string(),
|
||||||
playCount: z.number(),
|
playCount: z.number(),
|
||||||
playDate: z.string(),
|
playDate: z.string(),
|
||||||
rating: z.number(),
|
rating: z.number(),
|
||||||
size: z.number(),
|
size: z.number(),
|
||||||
smallImageUrl: z.string().optional(),
|
smallImageUrl: z.string().optional(),
|
||||||
songCount: z.number(),
|
songCount: z.number(),
|
||||||
starred: z.boolean(),
|
starred: z.boolean(),
|
||||||
starredAt: z.string(),
|
starredAt: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumArtistList = z.array(albumArtist);
|
const albumArtistList = z.array(albumArtist);
|
||||||
|
|
||||||
const ndAlbumArtistListSort = {
|
const ndAlbumArtistListSort = {
|
||||||
ALBUM_COUNT: 'albumCount',
|
ALBUM_COUNT: 'albumCount',
|
||||||
FAVORITED: 'starred ASC, starredAt ASC',
|
FAVORITED: 'starred ASC, starredAt ASC',
|
||||||
NAME: 'name',
|
NAME: 'name',
|
||||||
PLAY_COUNT: 'playCount',
|
PLAY_COUNT: 'playCount',
|
||||||
RATING: 'rating',
|
RATING: 'rating',
|
||||||
SONG_COUNT: 'songCount',
|
SONG_COUNT: 'songCount',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const albumArtistListParameters = paginationParameters.extend({
|
const albumArtistListParameters = paginationParameters.extend({
|
||||||
_sort: z.nativeEnum(ndAlbumArtistListSort).optional(),
|
_sort: z.nativeEnum(ndAlbumArtistListSort).optional(),
|
||||||
genre_id: z.string().optional(),
|
genre_id: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const album = z.object({
|
const album = z.object({
|
||||||
albumArtist: z.string(),
|
albumArtist: z.string(),
|
||||||
albumArtistId: z.string(),
|
albumArtistId: z.string(),
|
||||||
allArtistIds: z.string(),
|
allArtistIds: z.string(),
|
||||||
artist: z.string(),
|
artist: z.string(),
|
||||||
artistId: z.string(),
|
artistId: z.string(),
|
||||||
compilation: z.boolean(),
|
compilation: z.boolean(),
|
||||||
coverArtId: z.string().optional(), // Removed after v0.48.0
|
coverArtId: z.string().optional(), // Removed after v0.48.0
|
||||||
coverArtPath: z.string().optional(), // Removed after v0.48.0
|
coverArtPath: z.string().optional(), // Removed after v0.48.0
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
fullText: z.string(),
|
fullText: z.string(),
|
||||||
genre: z.string(),
|
genre: z.string(),
|
||||||
genres: z.array(genre),
|
genres: z.array(genre),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
maxYear: z.number(),
|
maxYear: z.number(),
|
||||||
mbzAlbumArtistId: z.string().optional(),
|
mbzAlbumArtistId: z.string().optional(),
|
||||||
mbzAlbumId: z.string().optional(),
|
mbzAlbumId: z.string().optional(),
|
||||||
minYear: z.number(),
|
minYear: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
orderAlbumArtistName: z.string(),
|
orderAlbumArtistName: z.string(),
|
||||||
orderAlbumName: z.string(),
|
orderAlbumName: z.string(),
|
||||||
playCount: z.number(),
|
playCount: z.number(),
|
||||||
playDate: z.string(),
|
playDate: z.string(),
|
||||||
rating: z.number().optional(),
|
rating: z.number().optional(),
|
||||||
size: z.number(),
|
size: z.number(),
|
||||||
songCount: z.number(),
|
songCount: z.number(),
|
||||||
sortAlbumArtistName: z.string(),
|
sortAlbumArtistName: z.string(),
|
||||||
sortArtistName: z.string(),
|
sortArtistName: z.string(),
|
||||||
starred: z.boolean(),
|
starred: z.boolean(),
|
||||||
starredAt: z.string().optional(),
|
starredAt: z.string().optional(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumList = z.array(album);
|
const albumList = z.array(album);
|
||||||
|
|
||||||
const ndAlbumListSort = {
|
const ndAlbumListSort = {
|
||||||
ALBUM_ARTIST: 'albumArtist',
|
ALBUM_ARTIST: 'albumArtist',
|
||||||
ARTIST: 'artist',
|
ARTIST: 'artist',
|
||||||
DURATION: 'duration',
|
DURATION: 'duration',
|
||||||
NAME: 'name',
|
NAME: 'name',
|
||||||
PLAY_COUNT: 'playCount',
|
PLAY_COUNT: 'playCount',
|
||||||
PLAY_DATE: 'play_date',
|
PLAY_DATE: 'play_date',
|
||||||
RANDOM: 'random',
|
RANDOM: 'random',
|
||||||
RATING: 'rating',
|
RATING: 'rating',
|
||||||
RECENTLY_ADDED: 'recently_added',
|
RECENTLY_ADDED: 'recently_added',
|
||||||
SONG_COUNT: 'songCount',
|
SONG_COUNT: 'songCount',
|
||||||
STARRED: 'starred',
|
STARRED: 'starred',
|
||||||
YEAR: 'max_year',
|
YEAR: 'max_year',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const albumListParameters = paginationParameters.extend({
|
const albumListParameters = paginationParameters.extend({
|
||||||
_sort: z.nativeEnum(ndAlbumListSort).optional(),
|
_sort: z.nativeEnum(ndAlbumListSort).optional(),
|
||||||
album_id: z.string().optional(),
|
album_id: z.string().optional(),
|
||||||
artist_id: z.string().optional(),
|
artist_id: z.string().optional(),
|
||||||
compilation: z.boolean().optional(),
|
compilation: z.boolean().optional(),
|
||||||
genre_id: z.string().optional(),
|
genre_id: z.string().optional(),
|
||||||
has_rating: z.boolean().optional(),
|
has_rating: z.boolean().optional(),
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
recently_added: z.boolean().optional(),
|
recently_added: z.boolean().optional(),
|
||||||
recently_played: z.boolean().optional(),
|
recently_played: z.boolean().optional(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const song = z.object({
|
const song = z.object({
|
||||||
album: z.string(),
|
album: z.string(),
|
||||||
albumArtist: z.string(),
|
albumArtist: z.string(),
|
||||||
albumArtistId: z.string(),
|
albumArtistId: z.string(),
|
||||||
albumId: z.string(),
|
albumId: z.string(),
|
||||||
artist: z.string(),
|
artist: z.string(),
|
||||||
artistId: z.string(),
|
artistId: z.string(),
|
||||||
bitRate: z.number(),
|
bitRate: z.number(),
|
||||||
bookmarkPosition: z.number(),
|
bookmarkPosition: z.number(),
|
||||||
bpm: z.number().optional(),
|
bpm: z.number().optional(),
|
||||||
channels: z.number().optional(),
|
channels: z.number().optional(),
|
||||||
comment: z.string().optional(),
|
comment: z.string().optional(),
|
||||||
compilation: z.boolean(),
|
compilation: z.boolean(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
discNumber: z.number(),
|
discNumber: z.number(),
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
fullText: z.string(),
|
fullText: z.string(),
|
||||||
genre: z.string(),
|
genre: z.string(),
|
||||||
genres: z.array(genre),
|
genres: z.array(genre),
|
||||||
hasCoverArt: z.boolean(),
|
hasCoverArt: z.boolean(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
lyrics: z.string().optional(),
|
lyrics: z.string().optional(),
|
||||||
mbzAlbumArtistId: z.string().optional(),
|
mbzAlbumArtistId: z.string().optional(),
|
||||||
mbzAlbumId: z.string().optional(),
|
mbzAlbumId: z.string().optional(),
|
||||||
mbzArtistId: z.string().optional(),
|
mbzArtistId: z.string().optional(),
|
||||||
mbzTrackId: z.string().optional(),
|
mbzTrackId: z.string().optional(),
|
||||||
orderAlbumArtistName: z.string(),
|
orderAlbumArtistName: z.string(),
|
||||||
orderAlbumName: z.string(),
|
orderAlbumName: z.string(),
|
||||||
orderArtistName: z.string(),
|
orderArtistName: z.string(),
|
||||||
orderTitle: z.string(),
|
orderTitle: z.string(),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
playCount: z.number(),
|
playCount: z.number(),
|
||||||
playDate: z.string(),
|
playDate: z.string(),
|
||||||
rating: z.number().optional(),
|
rating: z.number().optional(),
|
||||||
size: z.number(),
|
size: z.number(),
|
||||||
sortAlbumArtistName: z.string(),
|
sortAlbumArtistName: z.string(),
|
||||||
sortArtistName: z.string(),
|
sortArtistName: z.string(),
|
||||||
starred: z.boolean(),
|
starred: z.boolean(),
|
||||||
starredAt: z.string().optional(),
|
starredAt: z.string().optional(),
|
||||||
suffix: z.string(),
|
suffix: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
trackNumber: z.number(),
|
trackNumber: z.number(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
year: z.number(),
|
year: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const songList = z.array(song);
|
const songList = z.array(song);
|
||||||
|
|
||||||
const ndSongListSort = {
|
const ndSongListSort = {
|
||||||
ALBUM: 'album, order_album_artist_name, disc_number, track_number, title',
|
ALBUM: 'album, order_album_artist_name, disc_number, track_number, title',
|
||||||
ALBUM_ARTIST: 'order_album_artist_name, album, disc_number, track_number, title',
|
ALBUM_ARTIST: 'order_album_artist_name, album, disc_number, track_number, title',
|
||||||
ALBUM_SONGS: 'album, discNumber, trackNumber',
|
ALBUM_SONGS: 'album, discNumber, trackNumber',
|
||||||
ARTIST: 'artist',
|
ARTIST: 'artist',
|
||||||
BPM: 'bpm',
|
BPM: 'bpm',
|
||||||
CHANNELS: 'channels',
|
CHANNELS: 'channels',
|
||||||
COMMENT: 'comment',
|
COMMENT: 'comment',
|
||||||
DURATION: 'duration',
|
DURATION: 'duration',
|
||||||
FAVORITED: 'starred ASC, starredAt ASC',
|
FAVORITED: 'starred ASC, starredAt ASC',
|
||||||
GENRE: 'genre',
|
GENRE: 'genre',
|
||||||
ID: 'id',
|
ID: 'id',
|
||||||
PLAY_COUNT: 'playCount',
|
PLAY_COUNT: 'playCount',
|
||||||
PLAY_DATE: 'playDate',
|
PLAY_DATE: 'playDate',
|
||||||
RATING: 'rating',
|
RATING: 'rating',
|
||||||
RECENTLY_ADDED: 'createdAt',
|
RECENTLY_ADDED: 'createdAt',
|
||||||
TITLE: 'title',
|
TITLE: 'title',
|
||||||
TRACK: 'track',
|
TRACK: 'track',
|
||||||
YEAR: 'year, album, discNumber, trackNumber',
|
YEAR: 'year, album, discNumber, trackNumber',
|
||||||
};
|
};
|
||||||
|
|
||||||
const songListParameters = paginationParameters.extend({
|
const songListParameters = paginationParameters.extend({
|
||||||
_sort: z.nativeEnum(ndSongListSort).optional(),
|
_sort: z.nativeEnum(ndSongListSort).optional(),
|
||||||
album_artist_id: z.array(z.string()).optional(),
|
album_artist_id: z.array(z.string()).optional(),
|
||||||
album_id: z.array(z.string()).optional(),
|
album_id: z.array(z.string()).optional(),
|
||||||
artist_id: z.array(z.string()).optional(),
|
artist_id: z.array(z.string()).optional(),
|
||||||
genre_id: z.string().optional(),
|
genre_id: z.string().optional(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const playlist = z.object({
|
const playlist = z.object({
|
||||||
comment: z.string(),
|
comment: z.string(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
evaluatedAt: z.string(),
|
evaluatedAt: z.string(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
ownerId: z.string(),
|
ownerId: z.string(),
|
||||||
ownerName: z.string(),
|
ownerName: z.string(),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
public: z.boolean(),
|
public: z.boolean(),
|
||||||
rules: z.record(z.string(), z.any()),
|
rules: z.record(z.string(), z.any()),
|
||||||
size: z.number(),
|
size: z.number(),
|
||||||
songCount: z.number(),
|
songCount: z.number(),
|
||||||
sync: z.boolean(),
|
sync: z.boolean(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const playlistList = z.array(playlist);
|
const playlistList = z.array(playlist);
|
||||||
|
|
||||||
const ndPlaylistListSort = {
|
const ndPlaylistListSort = {
|
||||||
DURATION: 'duration',
|
DURATION: 'duration',
|
||||||
NAME: 'name',
|
NAME: 'name',
|
||||||
OWNER: 'ownerName',
|
OWNER: 'ownerName',
|
||||||
PUBLIC: 'public',
|
PUBLIC: 'public',
|
||||||
SONG_COUNT: 'songCount',
|
SONG_COUNT: 'songCount',
|
||||||
UPDATED_AT: 'updatedAt',
|
UPDATED_AT: 'updatedAt',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const playlistListParameters = paginationParameters.extend({
|
const playlistListParameters = paginationParameters.extend({
|
||||||
_sort: z.nativeEnum(ndPlaylistListSort).optional(),
|
_sort: z.nativeEnum(ndPlaylistListSort).optional(),
|
||||||
owner_id: z.string().optional(),
|
owner_id: z.string().optional(),
|
||||||
smart: z.boolean().optional(),
|
smart: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const playlistSong = song.extend({
|
const playlistSong = song.extend({
|
||||||
mediaFileId: z.string(),
|
mediaFileId: z.string(),
|
||||||
playlistId: z.string(),
|
playlistId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const playlistSongList = z.array(playlistSong);
|
const playlistSongList = z.array(playlistSong);
|
||||||
|
|
||||||
const createPlaylist = playlist.pick({
|
const createPlaylist = playlist.pick({
|
||||||
id: true,
|
id: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createPlaylistParameters = z.object({
|
const createPlaylistParameters = z.object({
|
||||||
comment: z.string().optional(),
|
comment: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
public: z.boolean().optional(),
|
public: z.boolean().optional(),
|
||||||
rules: z.record(z.any()).optional(),
|
rules: z.record(z.any()).optional(),
|
||||||
sync: z.boolean().optional(),
|
sync: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePlaylist = playlist;
|
const updatePlaylist = playlist;
|
||||||
@ -302,62 +302,62 @@ const updatePlaylistParameters = createPlaylistParameters.partial();
|
|||||||
const deletePlaylist = z.null();
|
const deletePlaylist = z.null();
|
||||||
|
|
||||||
const addToPlaylist = z.object({
|
const addToPlaylist = z.object({
|
||||||
added: z.number(),
|
added: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const addToPlaylistParameters = z.object({
|
const addToPlaylistParameters = z.object({
|
||||||
ids: z.array(z.string()),
|
ids: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeFromPlaylist = z.object({
|
const removeFromPlaylist = z.object({
|
||||||
ids: z.array(z.string()),
|
ids: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeFromPlaylistParameters = z.object({
|
const removeFromPlaylistParameters = z.object({
|
||||||
id: z.array(z.string()),
|
id: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ndType = {
|
export const ndType = {
|
||||||
_enum: {
|
_enum: {
|
||||||
albumArtistList: ndAlbumArtistListSort,
|
albumArtistList: ndAlbumArtistListSort,
|
||||||
albumList: ndAlbumListSort,
|
albumList: ndAlbumListSort,
|
||||||
playlistList: ndPlaylistListSort,
|
playlistList: ndPlaylistListSort,
|
||||||
songList: ndSongListSort,
|
songList: ndSongListSort,
|
||||||
userList: ndUserListSort,
|
userList: ndUserListSort,
|
||||||
},
|
},
|
||||||
_parameters: {
|
_parameters: {
|
||||||
addToPlaylist: addToPlaylistParameters,
|
addToPlaylist: addToPlaylistParameters,
|
||||||
albumArtistList: albumArtistListParameters,
|
albumArtistList: albumArtistListParameters,
|
||||||
albumList: albumListParameters,
|
albumList: albumListParameters,
|
||||||
authenticate: authenticateParameters,
|
authenticate: authenticateParameters,
|
||||||
createPlaylist: createPlaylistParameters,
|
createPlaylist: createPlaylistParameters,
|
||||||
playlistList: playlistListParameters,
|
playlistList: playlistListParameters,
|
||||||
removeFromPlaylist: removeFromPlaylistParameters,
|
removeFromPlaylist: removeFromPlaylistParameters,
|
||||||
songList: songListParameters,
|
songList: songListParameters,
|
||||||
updatePlaylist: updatePlaylistParameters,
|
updatePlaylist: updatePlaylistParameters,
|
||||||
userList: userListParameters,
|
userList: userListParameters,
|
||||||
},
|
},
|
||||||
_response: {
|
_response: {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
album,
|
album,
|
||||||
albumArtist,
|
albumArtist,
|
||||||
albumArtistList,
|
albumArtistList,
|
||||||
albumList,
|
albumList,
|
||||||
authenticate,
|
authenticate,
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
error,
|
error,
|
||||||
genre,
|
genre,
|
||||||
genreList,
|
genreList,
|
||||||
playlist,
|
playlist,
|
||||||
playlistList,
|
playlistList,
|
||||||
playlistSong,
|
playlistSong,
|
||||||
playlistSongList,
|
playlistSongList,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
song,
|
song,
|
||||||
songList,
|
songList,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
user,
|
user,
|
||||||
userList,
|
userList,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,132 +1,132 @@
|
|||||||
import { QueryFunctionContext } from '@tanstack/react-query';
|
import { QueryFunctionContext } from '@tanstack/react-query';
|
||||||
import { LyricSource } from './types';
|
import { LyricSource } from './types';
|
||||||
import type {
|
import type {
|
||||||
AlbumListQuery,
|
AlbumListQuery,
|
||||||
SongListQuery,
|
SongListQuery,
|
||||||
AlbumDetailQuery,
|
AlbumDetailQuery,
|
||||||
AlbumArtistListQuery,
|
AlbumArtistListQuery,
|
||||||
ArtistListQuery,
|
ArtistListQuery,
|
||||||
PlaylistListQuery,
|
PlaylistListQuery,
|
||||||
PlaylistDetailQuery,
|
PlaylistDetailQuery,
|
||||||
PlaylistSongListQuery,
|
PlaylistSongListQuery,
|
||||||
UserListQuery,
|
UserListQuery,
|
||||||
AlbumArtistDetailQuery,
|
AlbumArtistDetailQuery,
|
||||||
TopSongListQuery,
|
TopSongListQuery,
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SongDetailQuery,
|
SongDetailQuery,
|
||||||
RandomSongListQuery,
|
RandomSongListQuery,
|
||||||
LyricsQuery,
|
LyricsQuery,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const queryKeys: Record<
|
export const queryKeys: Record<
|
||||||
string,
|
string,
|
||||||
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
|
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
|
||||||
> = {
|
> = {
|
||||||
albumArtists: {
|
albumArtists: {
|
||||||
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
|
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
|
||||||
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
|
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
|
||||||
return [serverId, 'albumArtists', 'detail'] as const;
|
return [serverId, 'albumArtists', 'detail'] as const;
|
||||||
|
},
|
||||||
|
list: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||||
|
if (query) return [serverId, 'albumArtists', 'list', query] as const;
|
||||||
|
return [serverId, 'albumArtists', 'list'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'albumArtists'] as const,
|
||||||
|
topSongs: (serverId: string, query?: TopSongListQuery) => {
|
||||||
|
if (query) return [serverId, 'albumArtists', 'topSongs', query] as const;
|
||||||
|
return [serverId, 'albumArtists', 'topSongs'] as const;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
list: (serverId: string, query?: AlbumArtistListQuery) => {
|
albums: {
|
||||||
if (query) return [serverId, 'albumArtists', 'list', query] as const;
|
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||||
return [serverId, 'albumArtists', 'list'] as const;
|
[serverId, 'albums', 'detail', query] as const,
|
||||||
|
list: (serverId: string, query?: AlbumListQuery) => {
|
||||||
|
if (query) return [serverId, 'albums', 'list', query] as const;
|
||||||
|
return [serverId, 'albums', 'list'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'albums'],
|
||||||
|
serverRoot: (serverId: string) => [serverId, 'albums'],
|
||||||
|
songs: (serverId: string, query: SongListQuery) =>
|
||||||
|
[serverId, 'albums', 'songs', query] as const,
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'albumArtists'] as const,
|
artists: {
|
||||||
topSongs: (serverId: string, query?: TopSongListQuery) => {
|
list: (serverId: string, query?: ArtistListQuery) => {
|
||||||
if (query) return [serverId, 'albumArtists', 'topSongs', query] as const;
|
if (query) return [serverId, 'artists', 'list', query] as const;
|
||||||
return [serverId, 'albumArtists', 'topSongs'] as const;
|
return [serverId, 'artists', 'list'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'artists'] as const,
|
||||||
},
|
},
|
||||||
},
|
genres: {
|
||||||
albums: {
|
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
|
||||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
root: (serverId: string) => [serverId, 'genres'] as const,
|
||||||
[serverId, 'albums', 'detail', query] as const,
|
|
||||||
list: (serverId: string, query?: AlbumListQuery) => {
|
|
||||||
if (query) return [serverId, 'albums', 'list', query] as const;
|
|
||||||
return [serverId, 'albums', 'list'] as const;
|
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'albums'],
|
musicFolders: {
|
||||||
serverRoot: (serverId: string) => [serverId, 'albums'],
|
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
|
||||||
songs: (serverId: string, query: SongListQuery) =>
|
|
||||||
[serverId, 'albums', 'songs', query] as const,
|
|
||||||
},
|
|
||||||
artists: {
|
|
||||||
list: (serverId: string, query?: ArtistListQuery) => {
|
|
||||||
if (query) return [serverId, 'artists', 'list', query] as const;
|
|
||||||
return [serverId, 'artists', 'list'] as const;
|
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'artists'] as const,
|
playlists: {
|
||||||
},
|
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
|
||||||
genres: {
|
if (query) return [serverId, 'playlists', id, 'detail', query] as const;
|
||||||
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
|
if (id) return [serverId, 'playlists', id, 'detail'] as const;
|
||||||
root: (serverId: string) => [serverId, 'genres'] as const,
|
return [serverId, 'playlists', 'detail'] as const;
|
||||||
},
|
},
|
||||||
musicFolders: {
|
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
|
||||||
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
|
if (query) return [serverId, 'playlists', id, 'detailSongList', query] as const;
|
||||||
},
|
if (id) return [serverId, 'playlists', id, 'detailSongList'] as const;
|
||||||
playlists: {
|
return [serverId, 'playlists', 'detailSongList'] as const;
|
||||||
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
|
},
|
||||||
if (query) return [serverId, 'playlists', id, 'detail', query] as const;
|
list: (serverId: string, query?: PlaylistListQuery) => {
|
||||||
if (id) return [serverId, 'playlists', id, 'detail'] as const;
|
if (query) return [serverId, 'playlists', 'list', query] as const;
|
||||||
return [serverId, 'playlists', 'detail'] as const;
|
return [serverId, 'playlists', 'list'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'playlists'] as const,
|
||||||
|
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
|
||||||
|
if (query && id) return [serverId, 'playlists', id, 'songList', query] as const;
|
||||||
|
if (id) return [serverId, 'playlists', id, 'songList'] as const;
|
||||||
|
return [serverId, 'playlists', 'songList'] as const;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
|
search: {
|
||||||
if (query) return [serverId, 'playlists', id, 'detailSongList', query] as const;
|
list: (serverId: string, query?: SearchQuery) => {
|
||||||
if (id) return [serverId, 'playlists', id, 'detailSongList'] as const;
|
if (query) return [serverId, 'search', 'list', query] as const;
|
||||||
return [serverId, 'playlists', 'detailSongList'] as const;
|
return [serverId, 'search', 'list'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'search'] as const,
|
||||||
},
|
},
|
||||||
list: (serverId: string, query?: PlaylistListQuery) => {
|
server: {
|
||||||
if (query) return [serverId, 'playlists', 'list', query] as const;
|
root: (serverId: string) => [serverId] as const,
|
||||||
return [serverId, 'playlists', 'list'] as const;
|
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'playlists'] as const,
|
songs: {
|
||||||
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
|
detail: (serverId: string, query?: SongDetailQuery) => {
|
||||||
if (query && id) return [serverId, 'playlists', id, 'songList', query] as const;
|
if (query) return [serverId, 'songs', 'detail', query] as const;
|
||||||
if (id) return [serverId, 'playlists', id, 'songList'] as const;
|
return [serverId, 'songs', 'detail'] as const;
|
||||||
return [serverId, 'playlists', 'songList'] as const;
|
},
|
||||||
|
list: (serverId: string, query?: SongListQuery) => {
|
||||||
|
if (query) return [serverId, 'songs', 'list', query] as const;
|
||||||
|
return [serverId, 'songs', 'list'] as const;
|
||||||
|
},
|
||||||
|
lyrics: (serverId: string, query?: LyricsQuery) => {
|
||||||
|
if (query) return [serverId, 'song', 'lyrics', query] as const;
|
||||||
|
return [serverId, 'song', 'lyrics'] as const;
|
||||||
|
},
|
||||||
|
lyricsByRemoteId: (searchQuery: { remoteSongId: string; remoteSource: LyricSource }) => {
|
||||||
|
return ['song', 'lyrics', 'remote', searchQuery] as const;
|
||||||
|
},
|
||||||
|
lyricsSearch: (query?: LyricSearchQuery) => {
|
||||||
|
if (query) return ['lyrics', 'search', query] as const;
|
||||||
|
return ['lyrics', 'search'] as const;
|
||||||
|
},
|
||||||
|
randomSongList: (serverId: string, query?: RandomSongListQuery) => {
|
||||||
|
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
||||||
|
return [serverId, 'songs', 'randomSongList'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'songs'] as const,
|
||||||
},
|
},
|
||||||
},
|
users: {
|
||||||
search: {
|
list: (serverId: string, query?: UserListQuery) => {
|
||||||
list: (serverId: string, query?: SearchQuery) => {
|
if (query) return [serverId, 'users', 'list', query] as const;
|
||||||
if (query) return [serverId, 'search', 'list', query] as const;
|
return [serverId, 'users', 'list'] as const;
|
||||||
return [serverId, 'search', 'list'] as const;
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'users'] as const,
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'search'] as const,
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
root: (serverId: string) => [serverId] as const,
|
|
||||||
},
|
|
||||||
songs: {
|
|
||||||
detail: (serverId: string, query?: SongDetailQuery) => {
|
|
||||||
if (query) return [serverId, 'songs', 'detail', query] as const;
|
|
||||||
return [serverId, 'songs', 'detail'] as const;
|
|
||||||
},
|
|
||||||
list: (serverId: string, query?: SongListQuery) => {
|
|
||||||
if (query) return [serverId, 'songs', 'list', query] as const;
|
|
||||||
return [serverId, 'songs', 'list'] as const;
|
|
||||||
},
|
|
||||||
lyrics: (serverId: string, query?: LyricsQuery) => {
|
|
||||||
if (query) return [serverId, 'song', 'lyrics', query] as const;
|
|
||||||
return [serverId, 'song', 'lyrics'] as const;
|
|
||||||
},
|
|
||||||
lyricsByRemoteId: (searchQuery: { remoteSongId: string; remoteSource: LyricSource }) => {
|
|
||||||
return ['song', 'lyrics', 'remote', searchQuery] as const;
|
|
||||||
},
|
|
||||||
lyricsSearch: (query?: LyricSearchQuery) => {
|
|
||||||
if (query) return ['lyrics', 'search', query] as const;
|
|
||||||
return ['lyrics', 'search'] as const;
|
|
||||||
},
|
|
||||||
randomSongList: (serverId: string, query?: RandomSongListQuery) => {
|
|
||||||
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
|
|
||||||
return [serverId, 'songs', 'randomSongList'] as const;
|
|
||||||
},
|
|
||||||
root: (serverId: string) => [serverId, 'songs'] as const,
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
list: (serverId: string, query?: UserListQuery) => {
|
|
||||||
if (query) return [serverId, 'users', 'list', query] as const;
|
|
||||||
return [serverId, 'users', 'list'] as const;
|
|
||||||
},
|
|
||||||
root: (serverId: string) => [serverId, 'users'] as const,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -1,190 +1,190 @@
|
|||||||
export type SSBaseResponse = {
|
export type SSBaseResponse = {
|
||||||
serverVersion?: 'string';
|
serverVersion?: 'string';
|
||||||
status: 'string';
|
status: 'string';
|
||||||
type?: 'string';
|
type?: 'string';
|
||||||
version: 'string';
|
version: 'string';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSMusicFolderList = SSMusicFolder[];
|
export type SSMusicFolderList = SSMusicFolder[];
|
||||||
|
|
||||||
export type SSMusicFolderListResponse = {
|
export type SSMusicFolderListResponse = {
|
||||||
musicFolders: {
|
musicFolders: {
|
||||||
musicFolder: SSMusicFolder[];
|
musicFolder: SSMusicFolder[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSGenreList = SSGenre[];
|
export type SSGenreList = SSGenre[];
|
||||||
|
|
||||||
export type SSGenreListResponse = {
|
export type SSGenreListResponse = {
|
||||||
genres: {
|
genres: {
|
||||||
genre: SSGenre[];
|
genre: SSGenre[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistDetailParams = {
|
export type SSAlbumArtistDetailParams = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
|
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
|
||||||
|
|
||||||
export type SSAlbumArtistDetailResponse = {
|
export type SSAlbumArtistDetailResponse = {
|
||||||
artist: SSAlbumArtistListEntry & {
|
artist: SSAlbumArtistListEntry & {
|
||||||
album: SSAlbumListEntry[];
|
album: SSAlbumListEntry[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistList = {
|
export type SSAlbumArtistList = {
|
||||||
items: SSAlbumArtistListEntry[];
|
items: SSAlbumArtistListEntry[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number | null;
|
totalRecordCount: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistListResponse = {
|
export type SSAlbumArtistListResponse = {
|
||||||
artists: {
|
artists: {
|
||||||
ignoredArticles: string;
|
ignoredArticles: string;
|
||||||
index: SSArtistIndex[];
|
index: SSArtistIndex[];
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumList = {
|
export type SSAlbumList = {
|
||||||
items: SSAlbumListEntry[];
|
items: SSAlbumListEntry[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number | null;
|
totalRecordCount: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumListResponse = {
|
export type SSAlbumListResponse = {
|
||||||
albumList2: {
|
albumList2: {
|
||||||
album: SSAlbumListEntry[];
|
album: SSAlbumListEntry[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
|
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
|
||||||
|
|
||||||
export type SSAlbumDetailResponse = {
|
export type SSAlbumDetailResponse = {
|
||||||
album: SSAlbum;
|
album: SSAlbum;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSArtistInfoParams = {
|
export type SSArtistInfoParams = {
|
||||||
count?: number;
|
count?: number;
|
||||||
id: string;
|
id: string;
|
||||||
includeNotPresent?: boolean;
|
includeNotPresent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSArtistInfoResponse = {
|
export type SSArtistInfoResponse = {
|
||||||
artistInfo2: SSArtistInfo;
|
artistInfo2: SSArtistInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSArtistInfo = {
|
export type SSArtistInfo = {
|
||||||
biography: string;
|
biography: string;
|
||||||
largeImageUrl?: string;
|
largeImageUrl?: string;
|
||||||
lastFmUrl?: string;
|
lastFmUrl?: string;
|
||||||
mediumImageUrl?: string;
|
mediumImageUrl?: string;
|
||||||
musicBrainzId?: string;
|
musicBrainzId?: string;
|
||||||
similarArtist?: {
|
similarArtist?: {
|
||||||
|
albumCount: string;
|
||||||
|
artistImageUrl?: string;
|
||||||
|
coverArt?: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
smallImageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSMusicFolder = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSGenre = {
|
||||||
|
albumCount?: number;
|
||||||
|
songCount?: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSArtistIndex = {
|
||||||
|
artist: SSAlbumArtistListEntry[];
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSAlbumArtistListEntry = {
|
||||||
albumCount: string;
|
albumCount: string;
|
||||||
artistImageUrl?: string;
|
artistImageUrl?: string;
|
||||||
coverArt?: string;
|
coverArt?: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
|
||||||
smallImageUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSMusicFolder = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSGenre = {
|
|
||||||
albumCount?: number;
|
|
||||||
songCount?: number;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSArtistIndex = {
|
|
||||||
artist: SSAlbumArtistListEntry[];
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSAlbumArtistListEntry = {
|
|
||||||
albumCount: string;
|
|
||||||
artistImageUrl?: string;
|
|
||||||
coverArt?: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumListEntry = {
|
export type SSAlbumListEntry = {
|
||||||
album: string;
|
album: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
artistId: string;
|
artistId: string;
|
||||||
coverArt: string;
|
coverArt: string;
|
||||||
created: string;
|
created: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
genre?: string;
|
genre?: string;
|
||||||
id: string;
|
id: string;
|
||||||
isDir: boolean;
|
isDir: boolean;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
parent: string;
|
parent: string;
|
||||||
songCount: number;
|
songCount: number;
|
||||||
starred?: boolean;
|
starred?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
userRating?: number;
|
userRating?: number;
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbum = {
|
export type SSAlbum = {
|
||||||
song: SSSong[];
|
song: SSSong[];
|
||||||
} & SSAlbumListEntry;
|
} & SSAlbumListEntry;
|
||||||
|
|
||||||
export type SSSong = {
|
export type SSSong = {
|
||||||
album: string;
|
album: string;
|
||||||
albumId: string;
|
albumId: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
artistId?: string;
|
artistId?: string;
|
||||||
bitRate: number;
|
bitRate: number;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
coverArt: string;
|
coverArt: string;
|
||||||
created: string;
|
created: string;
|
||||||
discNumber?: number;
|
discNumber?: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
genre: string;
|
genre: string;
|
||||||
id: string;
|
id: string;
|
||||||
isDir: boolean;
|
isDir: boolean;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
parent: string;
|
parent: string;
|
||||||
path: string;
|
path: string;
|
||||||
playCount: number;
|
playCount: number;
|
||||||
size: number;
|
size: number;
|
||||||
starred?: boolean;
|
starred?: boolean;
|
||||||
suffix: string;
|
suffix: string;
|
||||||
title: string;
|
title: string;
|
||||||
track: number;
|
track: number;
|
||||||
type: string;
|
type: string;
|
||||||
userRating?: number;
|
userRating?: number;
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumListParams = {
|
export type SSAlbumListParams = {
|
||||||
fromYear?: number;
|
fromYear?: number;
|
||||||
genre?: string;
|
genre?: string;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
toYear?: number;
|
toYear?: number;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistListParams = {
|
export type SSAlbumArtistListParams = {
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSFavoriteParams = {
|
export type SSFavoriteParams = {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
artistId?: string;
|
artistId?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSFavorite = null;
|
export type SSFavorite = null;
|
||||||
@ -192,8 +192,8 @@ export type SSFavorite = null;
|
|||||||
export type SSFavoriteResponse = null;
|
export type SSFavoriteResponse = null;
|
||||||
|
|
||||||
export type SSRatingParams = {
|
export type SSRatingParams = {
|
||||||
id: string;
|
id: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSRating = null;
|
export type SSRating = null;
|
||||||
@ -201,24 +201,24 @@ export type SSRating = null;
|
|||||||
export type SSRatingResponse = null;
|
export type SSRatingResponse = null;
|
||||||
|
|
||||||
export type SSTopSongListParams = {
|
export type SSTopSongListParams = {
|
||||||
artist: string;
|
artist: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSTopSongListResponse = {
|
export type SSTopSongListResponse = {
|
||||||
topSongs: {
|
topSongs: {
|
||||||
song: SSSong[];
|
song: SSSong[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSTopSongList = {
|
export type SSTopSongList = {
|
||||||
items: SSSong[];
|
items: SSSong[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number | null;
|
totalRecordCount: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSScrobbleParams = {
|
export type SSScrobbleParams = {
|
||||||
id: string;
|
id: string;
|
||||||
submission?: boolean;
|
submission?: boolean;
|
||||||
time?: number;
|
time?: number;
|
||||||
};
|
};
|
||||||
|
@ -10,196 +10,198 @@ import { toast } from '/@/renderer/components/toast/index';
|
|||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
export const contract = c.router({
|
export const contract = c.router({
|
||||||
authenticate: {
|
authenticate: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'ping.view',
|
path: 'ping.view',
|
||||||
query: ssType._parameters.authenticate,
|
query: ssType._parameters.authenticate,
|
||||||
responses: {
|
responses: {
|
||||||
200: ssType._response.authenticate,
|
200: ssType._response.authenticate,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
createFavorite: {
|
||||||
createFavorite: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'star.view',
|
||||||
path: 'star.view',
|
query: ssType._parameters.createFavorite,
|
||||||
query: ssType._parameters.createFavorite,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.createFavorite,
|
||||||
200: ssType._response.createFavorite,
|
},
|
||||||
},
|
},
|
||||||
},
|
getArtistInfo: {
|
||||||
getArtistInfo: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'getArtistInfo.view',
|
||||||
path: 'getArtistInfo.view',
|
query: ssType._parameters.artistInfo,
|
||||||
query: ssType._parameters.artistInfo,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.artistInfo,
|
||||||
200: ssType._response.artistInfo,
|
},
|
||||||
},
|
},
|
||||||
},
|
getMusicFolderList: {
|
||||||
getMusicFolderList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'getMusicFolders.view',
|
||||||
path: 'getMusicFolders.view',
|
responses: {
|
||||||
responses: {
|
200: ssType._response.musicFolderList,
|
||||||
200: ssType._response.musicFolderList,
|
},
|
||||||
},
|
},
|
||||||
},
|
getRandomSongList: {
|
||||||
getRandomSongList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'getRandomSongs.view',
|
||||||
path: 'getRandomSongs.view',
|
query: ssType._parameters.randomSongList,
|
||||||
query: ssType._parameters.randomSongList,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.randomSongList,
|
||||||
200: ssType._response.randomSongList,
|
},
|
||||||
},
|
},
|
||||||
},
|
getTopSongsList: {
|
||||||
getTopSongsList: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'getTopSongs.view',
|
||||||
path: 'getTopSongs.view',
|
query: ssType._parameters.topSongsList,
|
||||||
query: ssType._parameters.topSongsList,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.topSongsList,
|
||||||
200: ssType._response.topSongsList,
|
},
|
||||||
},
|
},
|
||||||
},
|
removeFavorite: {
|
||||||
removeFavorite: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'unstar.view',
|
||||||
path: 'unstar.view',
|
query: ssType._parameters.removeFavorite,
|
||||||
query: ssType._parameters.removeFavorite,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.removeFavorite,
|
||||||
200: ssType._response.removeFavorite,
|
},
|
||||||
},
|
},
|
||||||
},
|
scrobble: {
|
||||||
scrobble: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'scrobble.view',
|
||||||
path: 'scrobble.view',
|
query: ssType._parameters.scrobble,
|
||||||
query: ssType._parameters.scrobble,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.scrobble,
|
||||||
200: ssType._response.scrobble,
|
},
|
||||||
},
|
},
|
||||||
},
|
search3: {
|
||||||
search3: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'search3.view',
|
||||||
path: 'search3.view',
|
query: ssType._parameters.search3,
|
||||||
query: ssType._parameters.search3,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.search3,
|
||||||
200: ssType._response.search3,
|
},
|
||||||
},
|
},
|
||||||
},
|
setRating: {
|
||||||
setRating: {
|
method: 'GET',
|
||||||
method: 'GET',
|
path: 'setRating.view',
|
||||||
path: 'setRating.view',
|
query: ssType._parameters.setRating,
|
||||||
query: ssType._parameters.setRating,
|
responses: {
|
||||||
responses: {
|
200: ssType._response.setRating,
|
||||||
200: ssType._response.setRating,
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosClient = axios.create({});
|
const axiosClient = axios.create({});
|
||||||
|
|
||||||
axiosClient.defaults.paramsSerializer = (params) => {
|
axiosClient.defaults.paramsSerializer = (params) => {
|
||||||
return qs.stringify(params, { arrayFormat: 'repeat' });
|
return qs.stringify(params, { arrayFormat: 'repeat' });
|
||||||
};
|
};
|
||||||
|
|
||||||
axiosClient.interceptors.response.use(
|
axiosClient.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
if (data['subsonic-response'].status !== 'ok') {
|
if (data['subsonic-response'].status !== 'ok') {
|
||||||
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
||||||
if (data['subsonic-response'].error.code !== 0) {
|
if (data['subsonic-response'].error.code !== 0) {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: data['subsonic-response'].error.message,
|
message: data['subsonic-response'].error.message,
|
||||||
title: 'Issue from Subsonic API',
|
title: 'Issue from Subsonic API',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const parsePath = (fullPath: string) => {
|
const parsePath = (fullPath: string) => {
|
||||||
const [path, params] = fullPath.split('?');
|
const [path, params] = fullPath.split('?');
|
||||||
|
|
||||||
const parsedParams = qs.parse(params);
|
const parsedParams = qs.parse(params);
|
||||||
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: notNilParams,
|
params: notNilParams,
|
||||||
path,
|
path,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ssApiClient = (args: {
|
export const ssApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
url?: string;
|
url?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { server, url, signal } = args;
|
const { server, url, signal } = args;
|
||||||
|
|
||||||
return initClient(contract, {
|
return initClient(contract, {
|
||||||
api: async ({ path, method, headers, body }) => {
|
api: async ({ path, method, headers, body }) => {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
const authParams: Record<string, any> = {};
|
const authParams: Record<string, any> = {};
|
||||||
|
|
||||||
const { params, path: api } = parsePath(path);
|
const { params, path: api } = parsePath(path);
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
baseUrl = `${server.url}/rest`;
|
baseUrl = `${server.url}/rest`;
|
||||||
const token = server.credential;
|
const token = server.credential;
|
||||||
const params = token.split(/&?\w=/gm);
|
const params = token.split(/&?\w=/gm);
|
||||||
|
|
||||||
authParams.u = server.username;
|
authParams.u = server.username;
|
||||||
if (params?.length === 4) {
|
if (params?.length === 4) {
|
||||||
authParams.s = params[2];
|
authParams.s = params[2];
|
||||||
authParams.t = params[3];
|
authParams.t = params[3];
|
||||||
} else if (params?.length === 3) {
|
} else if (params?.length === 3) {
|
||||||
authParams.p = params[2];
|
authParams.p = params[2];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
baseUrl = url;
|
baseUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await axiosClient.request<z.infer<typeof ssType._response.baseResponse>>({
|
const result = await axiosClient.request<
|
||||||
data: body,
|
z.infer<typeof ssType._response.baseResponse>
|
||||||
headers,
|
>({
|
||||||
method: method as Method,
|
data: body,
|
||||||
params: {
|
headers,
|
||||||
c: 'Feishin',
|
method: method as Method,
|
||||||
f: 'json',
|
params: {
|
||||||
v: '1.13.0',
|
c: 'Feishin',
|
||||||
...authParams,
|
f: 'json',
|
||||||
...params,
|
v: '1.13.0',
|
||||||
},
|
...authParams,
|
||||||
signal,
|
...params,
|
||||||
url: `${baseUrl}/${api}`,
|
},
|
||||||
});
|
signal,
|
||||||
|
url: `${baseUrl}/${api}`,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body: result.data['subsonic-response'],
|
body: result.data['subsonic-response'],
|
||||||
headers: result.headers as any,
|
headers: result.headers as any,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
} catch (e: Error | AxiosError | any) {
|
} catch (e: Error | AxiosError | any) {
|
||||||
console.log('CATCH ERR');
|
console.log('CATCH ERR');
|
||||||
|
|
||||||
if (isAxiosError(e)) {
|
if (isAxiosError(e)) {
|
||||||
const error = e as AxiosError;
|
const error = e as AxiosError;
|
||||||
const response = error.response as AxiosResponse;
|
const response = error.response as AxiosResponse;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body: response?.data,
|
body: response?.data,
|
||||||
headers: response.headers as any,
|
headers: response.headers as any,
|
||||||
status: response?.status,
|
status: response?.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
baseHeaders: {
|
baseHeaders: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -4,91 +4,91 @@ import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
|||||||
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
||||||
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
import {
|
import {
|
||||||
ArtistInfoArgs,
|
ArtistInfoArgs,
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
FavoriteArgs,
|
FavoriteArgs,
|
||||||
FavoriteResponse,
|
FavoriteResponse,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
MusicFolderListArgs,
|
MusicFolderListArgs,
|
||||||
MusicFolderListResponse,
|
MusicFolderListResponse,
|
||||||
SetRatingArgs,
|
SetRatingArgs,
|
||||||
RatingResponse,
|
RatingResponse,
|
||||||
ScrobbleArgs,
|
ScrobbleArgs,
|
||||||
ScrobbleResponse,
|
ScrobbleResponse,
|
||||||
SongListResponse,
|
SongListResponse,
|
||||||
TopSongListArgs,
|
TopSongListArgs,
|
||||||
SearchArgs,
|
SearchArgs,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
RandomSongListResponse,
|
RandomSongListResponse,
|
||||||
RandomSongListArgs,
|
RandomSongListArgs,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { randomString } from '/@/renderer/utils';
|
import { randomString } from '/@/renderer/utils';
|
||||||
|
|
||||||
const authenticate = async (
|
const authenticate = async (
|
||||||
url: string,
|
url: string,
|
||||||
body: {
|
body: {
|
||||||
legacy?: boolean;
|
legacy?: boolean;
|
||||||
password: string;
|
password: string;
|
||||||
username: string;
|
username: string;
|
||||||
},
|
|
||||||
): Promise<AuthenticationResponse> => {
|
|
||||||
let credential: string;
|
|
||||||
let credentialParams: {
|
|
||||||
p?: string;
|
|
||||||
s?: string;
|
|
||||||
t?: string;
|
|
||||||
u: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanServerUrl = url.replace(/\/$/, '');
|
|
||||||
|
|
||||||
if (body.legacy) {
|
|
||||||
credential = `u=${body.username}&p=${body.password}`;
|
|
||||||
credentialParams = {
|
|
||||||
p: body.password,
|
|
||||||
u: body.username,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const salt = randomString(12);
|
|
||||||
const hash = md5(body.password + salt);
|
|
||||||
credential = `u=${body.username}&s=${salt}&t=${hash}`;
|
|
||||||
credentialParams = {
|
|
||||||
s: salt,
|
|
||||||
t: hash,
|
|
||||||
u: body.username,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
|
||||||
query: {
|
|
||||||
c: 'Feishin',
|
|
||||||
f: 'json',
|
|
||||||
v: '1.13.0',
|
|
||||||
...credentialParams,
|
|
||||||
},
|
},
|
||||||
});
|
): Promise<AuthenticationResponse> => {
|
||||||
|
let credential: string;
|
||||||
|
let credentialParams: {
|
||||||
|
p?: string;
|
||||||
|
s?: string;
|
||||||
|
t?: string;
|
||||||
|
u: string;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
const cleanServerUrl = url.replace(/\/$/, '');
|
||||||
credential,
|
|
||||||
userId: null,
|
if (body.legacy) {
|
||||||
username: body.username,
|
credential = `u=${body.username}&p=${body.password}`;
|
||||||
};
|
credentialParams = {
|
||||||
|
p: body.password,
|
||||||
|
u: body.username,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const salt = randomString(12);
|
||||||
|
const hash = md5(body.password + salt);
|
||||||
|
credential = `u=${body.username}&s=${salt}&t=${hash}`;
|
||||||
|
credentialParams = {
|
||||||
|
s: salt,
|
||||||
|
t: hash,
|
||||||
|
u: body.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
||||||
|
query: {
|
||||||
|
c: 'Feishin',
|
||||||
|
f: 'json',
|
||||||
|
v: '1.13.0',
|
||||||
|
...credentialParams,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
credential,
|
||||||
|
userId: null,
|
||||||
|
username: body.username,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolderListResponse> => {
|
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolderListResponse> => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getMusicFolderList({});
|
const res = await ssApiClient(apiClientProps).getMusicFolderList({});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get music folder list');
|
throw new Error('Failed to get music folder list');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.musicFolders.musicFolder,
|
items: res.body.musicFolders.musicFolder,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const getAlbumArtistDetail = async (
|
// export const getAlbumArtistDetail = async (
|
||||||
@ -198,184 +198,185 @@ const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolde
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).createFavorite({
|
const res = await ssApiClient(apiClientProps).createFavorite({
|
||||||
query: {
|
query: {
|
||||||
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
||||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
||||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to create favorite');
|
throw new Error('Failed to create favorite');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
const removeFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).removeFavorite({
|
const res = await ssApiClient(apiClientProps).removeFavorite({
|
||||||
query: {
|
query: {
|
||||||
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
|
||||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
|
||||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to delete favorite');
|
throw new Error('Failed to delete favorite');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRating = async (args: SetRatingArgs): Promise<RatingResponse> => {
|
const setRating = async (args: SetRatingArgs): Promise<RatingResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const itemIds = query.item.map((item) => item.id);
|
const itemIds = query.item.map((item) => item.id);
|
||||||
|
|
||||||
for (const id of itemIds) {
|
for (const id of itemIds) {
|
||||||
await ssApiClient(apiClientProps).setRating({
|
await ssApiClient(apiClientProps).setRating({
|
||||||
query: {
|
query: {
|
||||||
id,
|
id,
|
||||||
rating: query.rating,
|
rating: query.rating,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse> => {
|
const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||||
query: {
|
query: {
|
||||||
artist: query.artist,
|
artist: query.artist,
|
||||||
count: query.limit,
|
count: query.limit,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get top songs');
|
throw new Error('Failed to get top songs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items:
|
items:
|
||||||
res.body.topSongs?.song?.map((song) => ssNormalize.song(song, apiClientProps.server, '')) ||
|
res.body.topSongs?.song?.map((song) =>
|
||||||
[],
|
ssNormalize.song(song, apiClientProps.server, ''),
|
||||||
startIndex: 0,
|
) || [],
|
||||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
startIndex: 0,
|
||||||
};
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getArtistInfo = async (
|
const getArtistInfo = async (
|
||||||
args: ArtistInfoArgs,
|
args: ArtistInfoArgs,
|
||||||
): Promise<z.infer<typeof ssType._response.artistInfo>> => {
|
): Promise<z.infer<typeof ssType._response.artistInfo>> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getArtistInfo({
|
const res = await ssApiClient(apiClientProps).getArtistInfo({
|
||||||
query: {
|
query: {
|
||||||
count: query.limit,
|
count: query.limit,
|
||||||
id: query.artistId,
|
id: query.artistId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get artist info');
|
throw new Error('Failed to get artist info');
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.body;
|
return res.body;
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrobble = async (args: ScrobbleArgs): Promise<ScrobbleResponse> => {
|
const scrobble = async (args: ScrobbleArgs): Promise<ScrobbleResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).scrobble({
|
const res = await ssApiClient(apiClientProps).scrobble({
|
||||||
query: {
|
query: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
submission: query.submission,
|
submission: query.submission,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to scrobble');
|
throw new Error('Failed to scrobble');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const search3 = async (args: SearchArgs): Promise<SearchResponse> => {
|
const search3 = async (args: SearchArgs): Promise<SearchResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).search3({
|
const res = await ssApiClient(apiClientProps).search3({
|
||||||
query: {
|
query: {
|
||||||
albumCount: query.albumLimit,
|
albumCount: query.albumLimit,
|
||||||
albumOffset: query.albumStartIndex,
|
albumOffset: query.albumStartIndex,
|
||||||
artistCount: query.albumArtistLimit,
|
artistCount: query.albumArtistLimit,
|
||||||
artistOffset: query.albumArtistStartIndex,
|
artistOffset: query.albumArtistStartIndex,
|
||||||
query: query.query,
|
query: query.query,
|
||||||
songCount: query.songLimit,
|
songCount: query.songLimit,
|
||||||
songOffset: query.songStartIndex,
|
songOffset: query.songStartIndex,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to search');
|
throw new Error('Failed to search');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtists: res.body.searchResult3?.artist?.map((artist) =>
|
albumArtists: res.body.searchResult3?.artist?.map((artist) =>
|
||||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||||
),
|
),
|
||||||
albums: res.body.searchResult3?.album?.map((album) =>
|
albums: res.body.searchResult3?.album?.map((album) =>
|
||||||
ssNormalize.album(album, apiClientProps.server),
|
ssNormalize.album(album, apiClientProps.server),
|
||||||
),
|
),
|
||||||
songs: res.body.searchResult3?.song?.map((song) =>
|
songs: res.body.searchResult3?.song?.map((song) =>
|
||||||
ssNormalize.song(song, apiClientProps.server, ''),
|
ssNormalize.song(song, apiClientProps.server, ''),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomSongList = async (args: RandomSongListArgs): Promise<RandomSongListResponse> => {
|
const getRandomSongList = async (args: RandomSongListArgs): Promise<RandomSongListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getRandomSongList({
|
const res = await ssApiClient(apiClientProps).getRandomSongList({
|
||||||
query: {
|
query: {
|
||||||
fromYear: query.minYear,
|
fromYear: query.minYear,
|
||||||
genre: query.genre,
|
genre: query.genre,
|
||||||
musicFolderId: query.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
size: query.limit,
|
size: query.limit,
|
||||||
toYear: query.maxYear,
|
toYear: query.maxYear,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to get random songs');
|
throw new Error('Failed to get random songs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.randomSongs?.song?.map((song) =>
|
items: res.body.randomSongs?.song?.map((song) =>
|
||||||
ssNormalize.song(song, apiClientProps.server, ''),
|
ssNormalize.song(song, apiClientProps.server, ''),
|
||||||
),
|
),
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ssController = {
|
export const ssController = {
|
||||||
authenticate,
|
authenticate,
|
||||||
createFavorite,
|
createFavorite,
|
||||||
getArtistInfo,
|
getArtistInfo,
|
||||||
getMusicFolderList,
|
getMusicFolderList,
|
||||||
getRandomSongList,
|
getRandomSongList,
|
||||||
getTopSongList,
|
getTopSongList,
|
||||||
removeFavorite,
|
removeFavorite,
|
||||||
scrobble,
|
scrobble,
|
||||||
search3,
|
search3,
|
||||||
setRating,
|
setRating,
|
||||||
};
|
};
|
||||||
|
@ -5,176 +5,178 @@ import { QueueSong, LibraryItem, AlbumArtist, Album } from '/@/renderer/api/type
|
|||||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
const getCoverArtUrl = (args: {
|
||||||
baseUrl: string | undefined;
|
baseUrl: string | undefined;
|
||||||
coverArtId?: string;
|
coverArtId?: string;
|
||||||
credential: string | undefined;
|
credential: string | undefined;
|
||||||
size: number;
|
size: number;
|
||||||
}) => {
|
}) => {
|
||||||
const size = args.size ? args.size : 250;
|
const size = args.size ? args.size : 250;
|
||||||
|
|
||||||
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${args.baseUrl}/rest/getCoverArt.view` +
|
`${args.baseUrl}/rest/getCoverArt.view` +
|
||||||
`?id=${args.coverArtId}` +
|
`?id=${args.coverArtId}` +
|
||||||
`&${args.credential}` +
|
`&${args.credential}` +
|
||||||
'&v=1.13.0' +
|
'&v=1.13.0' +
|
||||||
'&c=feishin' +
|
'&c=feishin' +
|
||||||
`&size=${size}`
|
`&size=${size}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof ssType._response.song>,
|
item: z.infer<typeof ssType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
): QueueSong => {
|
): QueueSong => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 100,
|
size: 100,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
|
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
album: item.album || '',
|
album: item.album || '',
|
||||||
albumArtists: [
|
albumArtists: [
|
||||||
{
|
{
|
||||||
id: item.artistId || '',
|
id: item.artistId || '',
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: item.artist || '',
|
name: item.artist || '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
albumId: item.albumId || '',
|
albumId: item.albumId || '',
|
||||||
artistName: item.artist || '',
|
artistName: item.artist || '',
|
||||||
artists: [
|
artists: [
|
||||||
{
|
{
|
||||||
id: item.artistId || '',
|
id: item.artistId || '',
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: item.artist || '',
|
name: item.artist || '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bitRate: item.bitRate || 0,
|
bitRate: item.bitRate || 0,
|
||||||
bpm: null,
|
bpm: null,
|
||||||
channels: null,
|
channels: null,
|
||||||
comment: null,
|
comment: null,
|
||||||
compilation: null,
|
compilation: null,
|
||||||
container: item.contentType,
|
container: item.contentType,
|
||||||
createdAt: item.created,
|
createdAt: item.created,
|
||||||
discNumber: item.discNumber || 1,
|
discNumber: item.discNumber || 1,
|
||||||
duration: item.duration || 0,
|
duration: item.duration || 0,
|
||||||
genres: item.genre
|
genres: item.genre
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: item.genre,
|
id: item.genre,
|
||||||
name: item.genre,
|
name: item.genre,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imagePlaceholderUrl: null,
|
imagePlaceholderUrl: null,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
lastPlayedAt: null,
|
lastPlayedAt: null,
|
||||||
lyrics: null,
|
lyrics: null,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
playCount: item?.playCount || 0,
|
playCount: item?.playCount || 0,
|
||||||
releaseDate: null,
|
releaseDate: null,
|
||||||
releaseYear: item.year ? String(item.year) : null,
|
releaseYear: item.year ? String(item.year) : null,
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.SUBSONIC,
|
serverType: ServerType.SUBSONIC,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
streamUrl,
|
streamUrl,
|
||||||
trackNumber: item.track || 1,
|
trackNumber: item.track || 1,
|
||||||
uniqueId: nanoid(),
|
uniqueId: nanoid(),
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
userFavorite: item.starred || false,
|
userFavorite: item.starred || false,
|
||||||
userRating: item.userRating || null,
|
userRating: item.userRating || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbumArtist = (
|
const normalizeAlbumArtist = (
|
||||||
item: z.infer<typeof ssType._response.albumArtist>,
|
item: z.infer<typeof ssType._response.albumArtist>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 100,
|
size: 100,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: item.albumCount ? Number(item.albumCount) : 0,
|
albumCount: item.albumCount ? Number(item.albumCount) : 0,
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: null,
|
biography: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
genres: [],
|
genres: [],
|
||||||
id: item.id,
|
id: item.id,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
lastPlayedAt: null,
|
lastPlayedAt: null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
playCount: null,
|
playCount: null,
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.SUBSONIC,
|
serverType: ServerType.SUBSONIC,
|
||||||
similarArtists: [],
|
similarArtists: [],
|
||||||
songCount: null,
|
songCount: null,
|
||||||
userFavorite: false,
|
userFavorite: false,
|
||||||
userRating: null,
|
userRating: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof ssType._response.album>,
|
item: z.infer<typeof ssType._response.album>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
): Album => {
|
): Album => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 300,
|
size: 300,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
albumArtists: item.artistId
|
||||||
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
? [{ id: item.artistId, imageUrl: null, name: item.artist }]
|
||||||
backdropImageUrl: null,
|
: [],
|
||||||
createdAt: item.created,
|
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
||||||
duration: item.duration,
|
backdropImageUrl: null,
|
||||||
genres: item.genre ? [{ id: item.genre, name: item.genre }] : [],
|
createdAt: item.created,
|
||||||
id: item.id,
|
duration: item.duration,
|
||||||
imagePlaceholderUrl: null,
|
genres: item.genre ? [{ id: item.genre, name: item.genre }] : [],
|
||||||
imageUrl,
|
id: item.id,
|
||||||
isCompilation: null,
|
imagePlaceholderUrl: null,
|
||||||
itemType: LibraryItem.ALBUM,
|
imageUrl,
|
||||||
lastPlayedAt: null,
|
isCompilation: null,
|
||||||
name: item.name,
|
itemType: LibraryItem.ALBUM,
|
||||||
playCount: null,
|
lastPlayedAt: null,
|
||||||
releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null,
|
name: item.name,
|
||||||
releaseYear: item.year ? Number(item.year) : null,
|
playCount: null,
|
||||||
serverId: server?.id || 'unknown',
|
releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null,
|
||||||
serverType: ServerType.SUBSONIC,
|
releaseYear: item.year ? Number(item.year) : null,
|
||||||
size: null,
|
serverId: server?.id || 'unknown',
|
||||||
songCount: item.songCount,
|
serverType: ServerType.SUBSONIC,
|
||||||
songs: [],
|
size: null,
|
||||||
uniqueId: nanoid(),
|
songCount: item.songCount,
|
||||||
updatedAt: item.created,
|
songs: [],
|
||||||
userFavorite: item.starred || false,
|
uniqueId: nanoid(),
|
||||||
userRating: item.userRating || null,
|
updatedAt: item.created,
|
||||||
};
|
userFavorite: item.starred || false,
|
||||||
|
userRating: item.userRating || null,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ssNormalize = {
|
export const ssNormalize = {
|
||||||
album: normalizeAlbum,
|
album: normalizeAlbum,
|
||||||
albumArtist: normalizeAlbumArtist,
|
albumArtist: normalizeAlbumArtist,
|
||||||
song: normalizeSong,
|
song: normalizeSong,
|
||||||
};
|
};
|
||||||
|
@ -1,240 +1,240 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const baseResponse = z.object({
|
const baseResponse = z.object({
|
||||||
'subsonic-response': z.object({
|
'subsonic-response': z.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authenticate = z.null();
|
const authenticate = z.null();
|
||||||
|
|
||||||
const authenticateParameters = z.object({
|
const authenticateParameters = z.object({
|
||||||
c: z.string(),
|
c: z.string(),
|
||||||
f: z.string(),
|
f: z.string(),
|
||||||
p: z.string().optional(),
|
p: z.string().optional(),
|
||||||
s: z.string().optional(),
|
s: z.string().optional(),
|
||||||
t: z.string().optional(),
|
t: z.string().optional(),
|
||||||
u: z.string(),
|
u: z.string(),
|
||||||
v: z.string(),
|
v: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createFavoriteParameters = z.object({
|
const createFavoriteParameters = z.object({
|
||||||
albumId: z.array(z.string()).optional(),
|
albumId: z.array(z.string()).optional(),
|
||||||
artistId: z.array(z.string()).optional(),
|
artistId: z.array(z.string()).optional(),
|
||||||
id: z.array(z.string()).optional(),
|
id: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createFavorite = z.null();
|
const createFavorite = z.null();
|
||||||
|
|
||||||
const removeFavoriteParameters = z.object({
|
const removeFavoriteParameters = z.object({
|
||||||
albumId: z.array(z.string()).optional(),
|
albumId: z.array(z.string()).optional(),
|
||||||
artistId: z.array(z.string()).optional(),
|
artistId: z.array(z.string()).optional(),
|
||||||
id: z.array(z.string()).optional(),
|
id: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeFavorite = z.null();
|
const removeFavorite = z.null();
|
||||||
|
|
||||||
const setRatingParameters = z.object({
|
const setRatingParameters = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
rating: z.number(),
|
rating: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRating = z.null();
|
const setRating = z.null();
|
||||||
|
|
||||||
const musicFolder = z.object({
|
const musicFolder = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const musicFolderList = z.object({
|
const musicFolderList = z.object({
|
||||||
musicFolders: z.object({
|
musicFolders: z.object({
|
||||||
musicFolder: z.array(musicFolder),
|
musicFolder: z.array(musicFolder),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const song = z.object({
|
const song = z.object({
|
||||||
album: z.string().optional(),
|
album: z.string().optional(),
|
||||||
albumId: z.string().optional(),
|
albumId: z.string().optional(),
|
||||||
artist: z.string().optional(),
|
artist: z.string().optional(),
|
||||||
artistId: z.string().optional(),
|
artistId: z.string().optional(),
|
||||||
averageRating: z.number().optional(),
|
averageRating: z.number().optional(),
|
||||||
bitRate: z.number().optional(),
|
bitRate: z.number().optional(),
|
||||||
contentType: z.string(),
|
contentType: z.string(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
created: z.string(),
|
created: z.string(),
|
||||||
discNumber: z.number(),
|
discNumber: z.number(),
|
||||||
duration: z.number().optional(),
|
duration: z.number().optional(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isDir: z.boolean(),
|
isDir: z.boolean(),
|
||||||
isVideo: z.boolean(),
|
isVideo: z.boolean(),
|
||||||
parent: z.string(),
|
parent: z.string(),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
playCount: z.number().optional(),
|
playCount: z.number().optional(),
|
||||||
size: z.number(),
|
size: z.number(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
suffix: z.string(),
|
suffix: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
track: z.number().optional(),
|
track: z.number().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
userRating: z.number().optional(),
|
userRating: z.number().optional(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const album = z.object({
|
const album = z.object({
|
||||||
album: z.string(),
|
album: z.string(),
|
||||||
artist: z.string(),
|
artist: z.string(),
|
||||||
artistId: z.string(),
|
artistId: z.string(),
|
||||||
coverArt: z.string(),
|
coverArt: z.string(),
|
||||||
created: z.string(),
|
created: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isDir: z.boolean(),
|
isDir: z.boolean(),
|
||||||
isVideo: z.boolean(),
|
isVideo: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
parent: z.string(),
|
parent: z.string(),
|
||||||
song: z.array(song),
|
song: z.array(song),
|
||||||
songCount: z.number(),
|
songCount: z.number(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
userRating: z.number().optional(),
|
userRating: z.number().optional(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumListParameters = z.object({
|
const albumListParameters = z.object({
|
||||||
fromYear: z.number().optional(),
|
fromYear: z.number().optional(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
musicFolderId: z.string().optional(),
|
musicFolderId: z.string().optional(),
|
||||||
offset: z.number().optional(),
|
offset: z.number().optional(),
|
||||||
size: z.number().optional(),
|
size: z.number().optional(),
|
||||||
toYear: z.number().optional(),
|
toYear: z.number().optional(),
|
||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumList = z.array(album.omit({ song: true }));
|
const albumList = z.array(album.omit({ song: true }));
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
albumCount: z.string(),
|
albumCount: z.string(),
|
||||||
artistImageUrl: z.string().optional(),
|
artistImageUrl: z.string().optional(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumArtistList = z.object({
|
const albumArtistList = z.object({
|
||||||
artist: z.array(albumArtist),
|
artist: z.array(albumArtist),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const artistInfoParameters = z.object({
|
const artistInfoParameters = z.object({
|
||||||
count: z.number().optional(),
|
count: z.number().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
includeNotPresent: z.boolean().optional(),
|
includeNotPresent: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const artistInfo = z.object({
|
const artistInfo = z.object({
|
||||||
artistInfo: z.object({
|
artistInfo: z.object({
|
||||||
biography: z.string().optional(),
|
biography: z.string().optional(),
|
||||||
largeImageUrl: z.string().optional(),
|
largeImageUrl: z.string().optional(),
|
||||||
lastFmUrl: z.string().optional(),
|
lastFmUrl: z.string().optional(),
|
||||||
mediumImageUrl: z.string().optional(),
|
mediumImageUrl: z.string().optional(),
|
||||||
musicBrainzId: z.string().optional(),
|
musicBrainzId: z.string().optional(),
|
||||||
similarArtist: z.array(
|
similarArtist: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
albumCount: z.string(),
|
albumCount: z.string(),
|
||||||
artistImageUrl: z.string().optional(),
|
artistImageUrl: z.string().optional(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
smallImageUrl: z.string().optional(),
|
smallImageUrl: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const topSongsListParameters = z.object({
|
const topSongsListParameters = z.object({
|
||||||
artist: z.string(), // The name of the artist, not the artist ID
|
artist: z.string(), // The name of the artist, not the artist ID
|
||||||
count: z.number().optional(),
|
count: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const topSongsList = z.object({
|
const topSongsList = z.object({
|
||||||
topSongs: z.object({
|
topSongs: z.object({
|
||||||
song: z.array(song),
|
song: z.array(song),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrobbleParameters = z.object({
|
const scrobbleParameters = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
submission: z.boolean().optional(),
|
submission: z.boolean().optional(),
|
||||||
time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
|
time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrobble = z.null();
|
const scrobble = z.null();
|
||||||
|
|
||||||
const search3 = z.object({
|
const search3 = z.object({
|
||||||
searchResult3: z.object({
|
searchResult3: z.object({
|
||||||
album: z.array(album),
|
album: z.array(album),
|
||||||
artist: z.array(albumArtist),
|
artist: z.array(albumArtist),
|
||||||
song: z.array(song),
|
song: z.array(song),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const search3Parameters = z.object({
|
const search3Parameters = z.object({
|
||||||
albumCount: z.number().optional(),
|
albumCount: z.number().optional(),
|
||||||
albumOffset: z.number().optional(),
|
albumOffset: z.number().optional(),
|
||||||
artistCount: z.number().optional(),
|
artistCount: z.number().optional(),
|
||||||
artistOffset: z.number().optional(),
|
artistOffset: z.number().optional(),
|
||||||
musicFolderId: z.string().optional(),
|
musicFolderId: z.string().optional(),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
songCount: z.number().optional(),
|
songCount: z.number().optional(),
|
||||||
songOffset: z.number().optional(),
|
songOffset: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const randomSongListParameters = z.object({
|
const randomSongListParameters = z.object({
|
||||||
fromYear: z.number().optional(),
|
fromYear: z.number().optional(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
musicFolderId: z.string().optional(),
|
musicFolderId: z.string().optional(),
|
||||||
size: z.number().optional(),
|
size: z.number().optional(),
|
||||||
toYear: z.number().optional(),
|
toYear: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const randomSongList = z.object({
|
const randomSongList = z.object({
|
||||||
randomSongs: z.object({
|
randomSongs: z.object({
|
||||||
song: z.array(song),
|
song: z.array(song),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ssType = {
|
export const ssType = {
|
||||||
_parameters: {
|
_parameters: {
|
||||||
albumList: albumListParameters,
|
albumList: albumListParameters,
|
||||||
artistInfo: artistInfoParameters,
|
artistInfo: artistInfoParameters,
|
||||||
authenticate: authenticateParameters,
|
authenticate: authenticateParameters,
|
||||||
createFavorite: createFavoriteParameters,
|
createFavorite: createFavoriteParameters,
|
||||||
randomSongList: randomSongListParameters,
|
randomSongList: randomSongListParameters,
|
||||||
removeFavorite: removeFavoriteParameters,
|
removeFavorite: removeFavoriteParameters,
|
||||||
scrobble: scrobbleParameters,
|
scrobble: scrobbleParameters,
|
||||||
search3: search3Parameters,
|
search3: search3Parameters,
|
||||||
setRating: setRatingParameters,
|
setRating: setRatingParameters,
|
||||||
topSongsList: topSongsListParameters,
|
topSongsList: topSongsListParameters,
|
||||||
},
|
},
|
||||||
_response: {
|
_response: {
|
||||||
album,
|
album,
|
||||||
albumArtist,
|
albumArtist,
|
||||||
albumArtistList,
|
albumArtistList,
|
||||||
albumList,
|
albumList,
|
||||||
artistInfo,
|
artistInfo,
|
||||||
authenticate,
|
authenticate,
|
||||||
baseResponse,
|
baseResponse,
|
||||||
createFavorite,
|
createFavorite,
|
||||||
musicFolderList,
|
musicFolderList,
|
||||||
randomSongList,
|
randomSongList,
|
||||||
removeFavorite,
|
removeFavorite,
|
||||||
scrobble,
|
scrobble,
|
||||||
search3,
|
search3,
|
||||||
setRating,
|
setRating,
|
||||||
song,
|
song,
|
||||||
topSongsList,
|
topSongsList,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6,35 +6,35 @@ import { ServerListItem } from '/@/renderer/types';
|
|||||||
|
|
||||||
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
|
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
|
||||||
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
|
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
|
||||||
return z.object({
|
return z.object({
|
||||||
data: itemSchema,
|
data: itemSchema,
|
||||||
headers: z.instanceof(AxiosHeaders),
|
headers: z.instanceof(AxiosHeaders),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
|
export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
|
||||||
itemSchema: ItemType,
|
itemSchema: ItemType,
|
||||||
) => {
|
) => {
|
||||||
return z.object({
|
return z.object({
|
||||||
'subsonic-response': z
|
'subsonic-response': z
|
||||||
.object({
|
.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
})
|
})
|
||||||
.extend(itemSchema),
|
.extend(itemSchema),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authenticationFailure = (currentServer: ServerListItem | null) => {
|
export const authenticationFailure = (currentServer: ServerListItem | null) => {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: 'Your session has expired.',
|
message: 'Your session has expired.',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentServer) {
|
if (currentServer) {
|
||||||
const serverId = currentServer.id;
|
const serverId = currentServer.id;
|
||||||
const token = currentServer.ndCredential;
|
const token = currentServer.ndCredential;
|
||||||
console.log(`token is expired: ${token}`);
|
console.log(`token is expired: ${token}`);
|
||||||
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
||||||
useAuthStore.getState().actions.setCurrentServer(null);
|
useAuthStore.getState().actions.setCurrentServer(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -29,161 +29,161 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul
|
|||||||
const ipc = isElectron() ? window.electron.ipc : null;
|
const ipc = isElectron() ? window.electron.ipc : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const contentFont = useSettingsStore((state) => state.general.fontContent);
|
const contentFont = useSettingsStore((state) => state.general.fontContent);
|
||||||
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();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--content-font-family', contentFont);
|
root.style.setProperty('--content-font-family', contentFont);
|
||||||
}, [contentFont]);
|
}, [contentFont]);
|
||||||
|
|
||||||
// Start the mpv instance on startup
|
// Start the mpv instance on startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeMpv = async () => {
|
const initializeMpv = async () => {
|
||||||
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||||
const properties = {
|
const properties = {
|
||||||
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||||
|
};
|
||||||
|
|
||||||
|
mpvPlayer?.initialize({
|
||||||
|
extraParameters,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
|
||||||
|
mpvPlayer?.volume(properties.volume);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mpvPlayer?.initialize({
|
if (isElectron() && playbackType === PlaybackType.LOCAL) {
|
||||||
extraParameters,
|
initializeMpv();
|
||||||
properties,
|
|
||||||
});
|
|
||||||
|
|
||||||
mpvPlayer?.volume(properties.volume);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isElectron() && playbackType === PlaybackType.LOCAL) {
|
|
||||||
initializeMpv();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearQueue();
|
|
||||||
mpvPlayer?.stop();
|
|
||||||
mpvPlayer?.cleanup();
|
|
||||||
};
|
|
||||||
}, [clearQueue, playbackType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isElectron()) {
|
|
||||||
ipc?.send('set-global-shortcuts', bindings);
|
|
||||||
}
|
|
||||||
}, [bindings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isElectron()) {
|
|
||||||
mpvPlayer.restoreQueue();
|
|
||||||
|
|
||||||
mpvPlayerListener.rendererSaveQueue(() => {
|
|
||||||
const { current, queue } = usePlayerStore.getState();
|
|
||||||
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
|
||||||
current: {
|
|
||||||
...current,
|
|
||||||
status: PlayerStatus.PAUSED,
|
|
||||||
},
|
|
||||||
queue,
|
|
||||||
};
|
|
||||||
mpvPlayer.saveQueue(stateToSave);
|
|
||||||
});
|
|
||||||
|
|
||||||
mpvPlayerListener.rendererRestoreQueue((_event: any, data: Partial<PlayerState>) => {
|
|
||||||
const playerData = restoreQueue(data);
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
|
||||||
mpvPlayer.setQueue(playerData, true);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ipc?.removeAllListeners('renderer-player-restore-queue');
|
clearQueue();
|
||||||
ipc?.removeAllListeners('renderer-player-save-queue');
|
mpvPlayer?.stop();
|
||||||
};
|
mpvPlayer?.cleanup();
|
||||||
}, [playbackType, restoreQueue]);
|
};
|
||||||
|
}, [clearQueue, playbackType]);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<MantineProvider
|
if (isElectron()) {
|
||||||
withGlobalStyles
|
ipc?.send('set-global-shortcuts', bindings);
|
||||||
withNormalizeCSS
|
}
|
||||||
theme={{
|
}, [bindings]);
|
||||||
colorScheme: theme as 'light' | 'dark',
|
|
||||||
components: {
|
useEffect(() => {
|
||||||
Modal: {
|
if (isElectron()) {
|
||||||
styles: {
|
mpvPlayer.restoreQueue();
|
||||||
body: { background: 'var(--modal-bg)', padding: '1rem !important' },
|
|
||||||
close: { marginRight: '0.5rem' },
|
mpvPlayerListener.rendererSaveQueue(() => {
|
||||||
content: { borderRadius: '5px' },
|
const { current, queue } = usePlayerStore.getState();
|
||||||
header: {
|
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
||||||
background: 'var(--modal-header-bg)',
|
current: {
|
||||||
paddingBottom: '1rem',
|
...current,
|
||||||
},
|
status: PlayerStatus.PAUSED,
|
||||||
title: { fontSize: 'medium', fontWeight: 500 },
|
},
|
||||||
},
|
queue,
|
||||||
},
|
};
|
||||||
},
|
mpvPlayer.saveQueue(stateToSave);
|
||||||
defaultRadius: 'xs',
|
});
|
||||||
dir: 'ltr',
|
|
||||||
focusRing: 'auto',
|
mpvPlayerListener.rendererRestoreQueue((_event: any, data: Partial<PlayerState>) => {
|
||||||
focusRingStyles: {
|
const playerData = restoreQueue(data);
|
||||||
inputStyles: () => ({
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
border: '1px solid var(--primary-color)',
|
mpvPlayer.setQueue(playerData, true);
|
||||||
}),
|
}
|
||||||
resetStyles: () => ({ outline: 'none' }),
|
});
|
||||||
styles: () => ({
|
}
|
||||||
outline: '1px solid var(--primary-color)',
|
|
||||||
outlineOffset: '-1px',
|
return () => {
|
||||||
}),
|
ipc?.removeAllListeners('renderer-player-restore-queue');
|
||||||
},
|
ipc?.removeAllListeners('renderer-player-save-queue');
|
||||||
fontFamily: 'var(--content-font-family)',
|
};
|
||||||
fontSizes: {
|
}, [playbackType, restoreQueue]);
|
||||||
lg: '1.1rem',
|
|
||||||
md: '1rem',
|
return (
|
||||||
sm: '0.9rem',
|
<MantineProvider
|
||||||
xl: '1.5rem',
|
withGlobalStyles
|
||||||
xs: '0.8rem',
|
withNormalizeCSS
|
||||||
},
|
theme={{
|
||||||
headings: {
|
colorScheme: theme as 'light' | 'dark',
|
||||||
fontFamily: 'var(--content-font-family)',
|
components: {
|
||||||
fontWeight: 700,
|
Modal: {
|
||||||
},
|
styles: {
|
||||||
other: {},
|
body: { background: 'var(--modal-bg)', padding: '1rem !important' },
|
||||||
spacing: {
|
close: { marginRight: '0.5rem' },
|
||||||
lg: '2rem',
|
content: { borderRadius: '5px' },
|
||||||
md: '1rem',
|
header: {
|
||||||
sm: '0.5rem',
|
background: 'var(--modal-header-bg)',
|
||||||
xl: '4rem',
|
paddingBottom: '1rem',
|
||||||
xs: '0rem',
|
},
|
||||||
},
|
title: { fontSize: 'medium', fontWeight: 500 },
|
||||||
}}
|
},
|
||||||
>
|
},
|
||||||
<ModalsProvider
|
},
|
||||||
modalProps={{
|
defaultRadius: 'xs',
|
||||||
centered: true,
|
dir: 'ltr',
|
||||||
styles: {
|
focusRing: 'auto',
|
||||||
body: { position: 'relative' },
|
focusRingStyles: {
|
||||||
content: { overflow: 'auto' },
|
inputStyles: () => ({
|
||||||
},
|
border: '1px solid var(--primary-color)',
|
||||||
transitionProps: {
|
}),
|
||||||
duration: 300,
|
resetStyles: () => ({ outline: 'none' }),
|
||||||
exitDuration: 300,
|
styles: () => ({
|
||||||
transition: 'fade',
|
outline: '1px solid var(--primary-color)',
|
||||||
},
|
outlineOffset: '-1px',
|
||||||
}}
|
}),
|
||||||
modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal }}
|
},
|
||||||
>
|
fontFamily: 'var(--content-font-family)',
|
||||||
<PlayQueueHandlerContext.Provider value={{ handlePlayQueueAdd }}>
|
fontSizes: {
|
||||||
<ContextMenuProvider>
|
lg: '1.1rem',
|
||||||
<AppRouter />
|
md: '1rem',
|
||||||
</ContextMenuProvider>
|
sm: '0.9rem',
|
||||||
</PlayQueueHandlerContext.Provider>
|
xl: '1.5rem',
|
||||||
</ModalsProvider>
|
xs: '0.8rem',
|
||||||
</MantineProvider>
|
},
|
||||||
);
|
headings: {
|
||||||
|
fontFamily: 'var(--content-font-family)',
|
||||||
|
fontWeight: 700,
|
||||||
|
},
|
||||||
|
other: {},
|
||||||
|
spacing: {
|
||||||
|
lg: '2rem',
|
||||||
|
md: '1rem',
|
||||||
|
sm: '0.5rem',
|
||||||
|
xl: '4rem',
|
||||||
|
xs: '0rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalsProvider
|
||||||
|
modalProps={{
|
||||||
|
centered: true,
|
||||||
|
styles: {
|
||||||
|
body: { position: 'relative' },
|
||||||
|
content: { overflow: 'auto' },
|
||||||
|
},
|
||||||
|
transitionProps: {
|
||||||
|
duration: 300,
|
||||||
|
exitDuration: 300,
|
||||||
|
transition: 'fade',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal }}
|
||||||
|
>
|
||||||
|
<PlayQueueHandlerContext.Provider value={{ handlePlayQueueAdd }}>
|
||||||
|
<ContextMenuProvider>
|
||||||
|
<AppRouter />
|
||||||
|
</ContextMenuProvider>
|
||||||
|
</PlayQueueHandlerContext.Provider>
|
||||||
|
</ModalsProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,17 +5,17 @@ import styled from 'styled-components';
|
|||||||
type AccordionProps = MantineAccordionProps;
|
type AccordionProps = MantineAccordionProps;
|
||||||
|
|
||||||
const StyledAccordion = styled(MantineAccordion)`
|
const StyledAccordion = styled(MantineAccordion)`
|
||||||
& .mantine-Accordion-panel {
|
& .mantine-Accordion-panel {
|
||||||
background: var(--paper-bg);
|
background: var(--paper-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mantine-Accordion-control {
|
.mantine-Accordion-control {
|
||||||
background: var(--paper-bg);
|
background: var(--paper-bg);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Accordion = ({ children, ...props }: AccordionProps) => {
|
export const Accordion = ({ children, ...props }: AccordionProps) => {
|
||||||
return <StyledAccordion {...props}>{children}</StyledAccordion>;
|
return <StyledAccordion {...props}>{children}</StyledAccordion>;
|
||||||
};
|
};
|
||||||
|
|
||||||
Accordion.Control = StyledAccordion.Control;
|
Accordion.Control = StyledAccordion.Control;
|
||||||
|
@ -4,188 +4,192 @@ import type { ReactPlayerProps } from 'react-player';
|
|||||||
import ReactPlayer from 'react-player';
|
import ReactPlayer from 'react-player';
|
||||||
import type { Song } from '/@/renderer/api/types';
|
import type { Song } from '/@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
crossfadeHandler,
|
crossfadeHandler,
|
||||||
gaplessHandler,
|
gaplessHandler,
|
||||||
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
||||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import type { CrossfadeStyle } from '/@/renderer/types';
|
import type { CrossfadeStyle } from '/@/renderer/types';
|
||||||
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
||||||
|
|
||||||
interface AudioPlayerProps extends ReactPlayerProps {
|
interface AudioPlayerProps extends ReactPlayerProps {
|
||||||
crossfadeDuration: number;
|
crossfadeDuration: number;
|
||||||
crossfadeStyle: CrossfadeStyle;
|
crossfadeStyle: CrossfadeStyle;
|
||||||
currentPlayer: 1 | 2;
|
currentPlayer: 1 | 2;
|
||||||
playbackStyle: PlaybackStyle;
|
playbackStyle: PlaybackStyle;
|
||||||
player1: Song;
|
player1: Song;
|
||||||
player2: Song;
|
player2: Song;
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AudioPlayerProgress = {
|
export type AudioPlayerProgress = {
|
||||||
loaded: number;
|
loaded: number;
|
||||||
loadedSeconds: number;
|
loadedSeconds: number;
|
||||||
played: number;
|
played: number;
|
||||||
playedSeconds: number;
|
playedSeconds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDuration = (ref: any) => {
|
const getDuration = (ref: any) => {
|
||||||
return ref.current?.player?.player?.player?.duration;
|
return ref.current?.player?.player?.player?.duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AudioPlayer = forwardRef(
|
export const AudioPlayer = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
playbackStyle,
|
playbackStyle,
|
||||||
crossfadeStyle,
|
crossfadeStyle,
|
||||||
crossfadeDuration,
|
crossfadeDuration,
|
||||||
currentPlayer,
|
currentPlayer,
|
||||||
autoNext,
|
autoNext,
|
||||||
player1,
|
player1,
|
||||||
player2,
|
player2,
|
||||||
muted,
|
muted,
|
||||||
volume,
|
volume,
|
||||||
}: AudioPlayerProps,
|
}: AudioPlayerProps,
|
||||||
ref: any,
|
ref: any,
|
||||||
) => {
|
) => {
|
||||||
const player1Ref = useRef<any>(null);
|
const player1Ref = useRef<any>(null);
|
||||||
const player2Ref = useRef<any>(null);
|
const player2Ref = useRef<any>(null);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
get player1() {
|
get player1() {
|
||||||
return player1Ref?.current;
|
return player1Ref?.current;
|
||||||
},
|
},
|
||||||
get player2() {
|
get player2() {
|
||||||
return player2Ref?.current;
|
return player2Ref?.current;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleOnEnded = () => {
|
const handleOnEnded = () => {
|
||||||
autoNext();
|
autoNext();
|
||||||
setIsTransitioning(false);
|
setIsTransitioning(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === PlayerStatus.PLAYING) {
|
if (status === PlayerStatus.PLAYING) {
|
||||||
if (currentPlayer === 1) {
|
if (currentPlayer === 1) {
|
||||||
player1Ref.current?.getInternalPlayer()?.play();
|
player1Ref.current?.getInternalPlayer()?.play();
|
||||||
} else {
|
} else {
|
||||||
player2Ref.current?.getInternalPlayer()?.play();
|
player2Ref.current?.getInternalPlayer()?.play();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
player1Ref.current?.getInternalPlayer()?.pause();
|
player1Ref.current?.getInternalPlayer()?.pause();
|
||||||
player2Ref.current?.getInternalPlayer()?.pause();
|
player2Ref.current?.getInternalPlayer()?.pause();
|
||||||
}
|
}
|
||||||
}, [currentPlayer, status]);
|
}, [currentPlayer, status]);
|
||||||
|
|
||||||
const handleCrossfade1 = useCallback(
|
const handleCrossfade1 = useCallback(
|
||||||
(e: AudioPlayerProgress) => {
|
(e: AudioPlayerProgress) => {
|
||||||
return crossfadeHandler({
|
return crossfadeHandler({
|
||||||
currentPlayer,
|
currentPlayer,
|
||||||
currentPlayerRef: player1Ref,
|
currentPlayerRef: player1Ref,
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(player1Ref),
|
duration: getDuration(player1Ref),
|
||||||
fadeDuration: crossfadeDuration,
|
fadeDuration: crossfadeDuration,
|
||||||
fadeType: crossfadeStyle,
|
fadeType: crossfadeStyle,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayerRef: player2Ref,
|
nextPlayerRef: player2Ref,
|
||||||
player: 1,
|
player: 1,
|
||||||
setIsTransitioning,
|
setIsTransitioning,
|
||||||
volume,
|
volume,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
|
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCrossfade2 = useCallback(
|
const handleCrossfade2 = useCallback(
|
||||||
(e: AudioPlayerProgress) => {
|
(e: AudioPlayerProgress) => {
|
||||||
return crossfadeHandler({
|
return crossfadeHandler({
|
||||||
currentPlayer,
|
currentPlayer,
|
||||||
currentPlayerRef: player2Ref,
|
currentPlayerRef: player2Ref,
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(player2Ref),
|
duration: getDuration(player2Ref),
|
||||||
fadeDuration: crossfadeDuration,
|
fadeDuration: crossfadeDuration,
|
||||||
fadeType: crossfadeStyle,
|
fadeType: crossfadeStyle,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayerRef: player1Ref,
|
nextPlayerRef: player1Ref,
|
||||||
player: 2,
|
player: 2,
|
||||||
setIsTransitioning,
|
setIsTransitioning,
|
||||||
volume,
|
volume,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
|
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGapless1 = useCallback(
|
const handleGapless1 = useCallback(
|
||||||
(e: AudioPlayerProgress) => {
|
(e: AudioPlayerProgress) => {
|
||||||
return gaplessHandler({
|
return gaplessHandler({
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(player1Ref),
|
duration: getDuration(player1Ref),
|
||||||
isFlac: player1?.container === 'flac',
|
isFlac: player1?.container === 'flac',
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayerRef: player2Ref,
|
nextPlayerRef: player2Ref,
|
||||||
setIsTransitioning,
|
setIsTransitioning,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[isTransitioning, player1?.container],
|
[isTransitioning, player1?.container],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGapless2 = useCallback(
|
const handleGapless2 = useCallback(
|
||||||
(e: AudioPlayerProgress) => {
|
(e: AudioPlayerProgress) => {
|
||||||
return gaplessHandler({
|
return gaplessHandler({
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(player2Ref),
|
duration: getDuration(player2Ref),
|
||||||
isFlac: player2?.container === 'flac',
|
isFlac: player2?.container === 'flac',
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayerRef: player1Ref,
|
nextPlayerRef: player1Ref,
|
||||||
setIsTransitioning,
|
setIsTransitioning,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[isTransitioning, player2?.container],
|
[isTransitioning, player2?.container],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
if (audioDeviceId) {
|
if (audioDeviceId) {
|
||||||
player1Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
|
player1Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
|
||||||
player2Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
|
player2Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
|
||||||
} else {
|
} else {
|
||||||
player1Ref.current?.getInternalPlayer()?.setSinkId('');
|
player1Ref.current?.getInternalPlayer()?.setSinkId('');
|
||||||
player2Ref.current?.getInternalPlayer()?.setSinkId('');
|
player2Ref.current?.getInternalPlayer()?.setSinkId('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [audioDeviceId]);
|
}, [audioDeviceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
ref={player1Ref}
|
ref={player1Ref}
|
||||||
height={0}
|
height={0}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
||||||
progressInterval={isTransitioning ? 10 : 250}
|
progressInterval={isTransitioning ? 10 : 250}
|
||||||
url={player1?.streamUrl}
|
url={player1?.streamUrl}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
width={0}
|
width={0}
|
||||||
onEnded={handleOnEnded}
|
onEnded={handleOnEnded}
|
||||||
onProgress={playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1}
|
onProgress={
|
||||||
/>
|
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||||
<ReactPlayer
|
}
|
||||||
ref={player2Ref}
|
/>
|
||||||
height={0}
|
<ReactPlayer
|
||||||
muted={muted}
|
ref={player2Ref}
|
||||||
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
height={0}
|
||||||
progressInterval={isTransitioning ? 10 : 250}
|
muted={muted}
|
||||||
url={player2?.streamUrl}
|
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
||||||
volume={volume}
|
progressInterval={isTransitioning ? 10 : 250}
|
||||||
width={0}
|
url={player2?.streamUrl}
|
||||||
onEnded={handleOnEnded}
|
volume={volume}
|
||||||
onProgress={playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2}
|
width={0}
|
||||||
/>
|
onEnded={handleOnEnded}
|
||||||
</>
|
onProgress={
|
||||||
);
|
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||||
},
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
@ -3,129 +3,131 @@ import type { Dispatch } from 'react';
|
|||||||
import { CrossfadeStyle } from '/@/renderer/types';
|
import { CrossfadeStyle } from '/@/renderer/types';
|
||||||
|
|
||||||
export const gaplessHandler = (args: {
|
export const gaplessHandler = (args: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
isFlac: boolean;
|
isFlac: boolean;
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
nextPlayerRef: any;
|
nextPlayerRef: any;
|
||||||
setIsTransitioning: Dispatch<boolean>;
|
setIsTransitioning: Dispatch<boolean>;
|
||||||
}) => {
|
}) => {
|
||||||
const { nextPlayerRef, currentTime, duration, isTransitioning, setIsTransitioning, isFlac } =
|
const { nextPlayerRef, currentTime, duration, isTransitioning, setIsTransitioning, isFlac } =
|
||||||
args;
|
args;
|
||||||
|
|
||||||
if (!isTransitioning) {
|
if (!isTransitioning) {
|
||||||
if (currentTime > duration - 2) {
|
if (currentTime > duration - 2) {
|
||||||
return setIsTransitioning(true);
|
return setIsTransitioning(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationPadding = isFlac ? 0.065 : 0.116;
|
||||||
|
if (currentTime + durationPadding >= duration) {
|
||||||
|
return nextPlayerRef.current.getInternalPlayer().play();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const durationPadding = isFlac ? 0.065 : 0.116;
|
|
||||||
if (currentTime + durationPadding >= duration) {
|
|
||||||
return nextPlayerRef.current.getInternalPlayer().play();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const crossfadeHandler = (args: {
|
export const crossfadeHandler = (args: {
|
||||||
currentPlayer: 1 | 2;
|
currentPlayer: 1 | 2;
|
||||||
currentPlayerRef: any;
|
currentPlayerRef: any;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
fadeDuration: number;
|
fadeDuration: number;
|
||||||
fadeType: CrossfadeStyle;
|
fadeType: CrossfadeStyle;
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
nextPlayerRef: any;
|
nextPlayerRef: any;
|
||||||
player: 1 | 2;
|
player: 1 | 2;
|
||||||
setIsTransitioning: Dispatch<boolean>;
|
setIsTransitioning: Dispatch<boolean>;
|
||||||
volume: number;
|
volume: number;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
currentTime,
|
currentTime,
|
||||||
player,
|
player,
|
||||||
currentPlayer,
|
currentPlayer,
|
||||||
currentPlayerRef,
|
currentPlayerRef,
|
||||||
nextPlayerRef,
|
nextPlayerRef,
|
||||||
fadeDuration,
|
fadeDuration,
|
||||||
fadeType,
|
fadeType,
|
||||||
duration,
|
duration,
|
||||||
volume,
|
volume,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
setIsTransitioning,
|
setIsTransitioning,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
if (!isTransitioning || currentPlayer !== player) {
|
if (!isTransitioning || currentPlayer !== player) {
|
||||||
const shouldBeginTransition = currentTime >= duration - fadeDuration;
|
const shouldBeginTransition = currentTime >= duration - fadeDuration;
|
||||||
|
|
||||||
if (shouldBeginTransition) {
|
if (shouldBeginTransition) {
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
return nextPlayerRef.current.getInternalPlayer().play();
|
return nextPlayerRef.current.getInternalPlayer().play();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeLeft = duration - currentTime;
|
||||||
|
let currentPlayerVolumeCalculation;
|
||||||
|
let nextPlayerVolumeCalculation;
|
||||||
|
let percentageOfFadeLeft;
|
||||||
|
let n;
|
||||||
|
switch (fadeType) {
|
||||||
|
case 'equalPower':
|
||||||
|
// https://dsp.stackexchange.com/a/14755
|
||||||
|
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
|
||||||
|
currentPlayerVolumeCalculation = Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
|
||||||
|
nextPlayerVolumeCalculation = Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
|
||||||
|
break;
|
||||||
|
case 'linear':
|
||||||
|
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
||||||
|
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
|
||||||
|
break;
|
||||||
|
case 'dipped':
|
||||||
|
// https://math.stackexchange.com/a/4622
|
||||||
|
percentageOfFadeLeft = timeLeft / fadeDuration;
|
||||||
|
currentPlayerVolumeCalculation = percentageOfFadeLeft ** 2 * volume;
|
||||||
|
nextPlayerVolumeCalculation = (percentageOfFadeLeft - 1) ** 2 * volume;
|
||||||
|
break;
|
||||||
|
case fadeType.match(/constantPower.*/)?.input:
|
||||||
|
// https://math.stackexchange.com/a/26159
|
||||||
|
n =
|
||||||
|
fadeType === 'constantPower'
|
||||||
|
? 0
|
||||||
|
: fadeType === 'constantPowerSlowFade'
|
||||||
|
? 1
|
||||||
|
: fadeType === 'constantPowerSlowCut'
|
||||||
|
? 3
|
||||||
|
: 10;
|
||||||
|
|
||||||
|
percentageOfFadeLeft = timeLeft / fadeDuration;
|
||||||
|
currentPlayerVolumeCalculation =
|
||||||
|
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) - 1)) *
|
||||||
|
volume;
|
||||||
|
nextPlayerVolumeCalculation =
|
||||||
|
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)) *
|
||||||
|
volume;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
||||||
|
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPlayerVolume =
|
||||||
|
currentPlayerVolumeCalculation >= 0 ? currentPlayerVolumeCalculation : 0;
|
||||||
|
|
||||||
|
const nextPlayerVolume =
|
||||||
|
nextPlayerVolumeCalculation <= volume ? nextPlayerVolumeCalculation : volume;
|
||||||
|
|
||||||
|
if (currentPlayer === 1) {
|
||||||
|
currentPlayerRef.current.getInternalPlayer().volume = currentPlayerVolume;
|
||||||
|
nextPlayerRef.current.getInternalPlayer().volume = nextPlayerVolume;
|
||||||
|
} else {
|
||||||
|
currentPlayerRef.current.getInternalPlayer().volume = currentPlayerVolume;
|
||||||
|
nextPlayerRef.current.getInternalPlayer().volume = nextPlayerVolume;
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const timeLeft = duration - currentTime;
|
|
||||||
let currentPlayerVolumeCalculation;
|
|
||||||
let nextPlayerVolumeCalculation;
|
|
||||||
let percentageOfFadeLeft;
|
|
||||||
let n;
|
|
||||||
switch (fadeType) {
|
|
||||||
case 'equalPower':
|
|
||||||
// https://dsp.stackexchange.com/a/14755
|
|
||||||
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
|
|
||||||
currentPlayerVolumeCalculation = Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
|
|
||||||
nextPlayerVolumeCalculation = Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
|
|
||||||
break;
|
|
||||||
case 'linear':
|
|
||||||
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
|
||||||
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
|
|
||||||
break;
|
|
||||||
case 'dipped':
|
|
||||||
// https://math.stackexchange.com/a/4622
|
|
||||||
percentageOfFadeLeft = timeLeft / fadeDuration;
|
|
||||||
currentPlayerVolumeCalculation = percentageOfFadeLeft ** 2 * volume;
|
|
||||||
nextPlayerVolumeCalculation = (percentageOfFadeLeft - 1) ** 2 * volume;
|
|
||||||
break;
|
|
||||||
case fadeType.match(/constantPower.*/)?.input:
|
|
||||||
// https://math.stackexchange.com/a/26159
|
|
||||||
n =
|
|
||||||
fadeType === 'constantPower'
|
|
||||||
? 0
|
|
||||||
: fadeType === 'constantPowerSlowFade'
|
|
||||||
? 1
|
|
||||||
: fadeType === 'constantPowerSlowCut'
|
|
||||||
? 3
|
|
||||||
: 10;
|
|
||||||
|
|
||||||
percentageOfFadeLeft = timeLeft / fadeDuration;
|
|
||||||
currentPlayerVolumeCalculation =
|
|
||||||
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) - 1)) * volume;
|
|
||||||
nextPlayerVolumeCalculation =
|
|
||||||
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)) * volume;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
|
|
||||||
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPlayerVolume =
|
|
||||||
currentPlayerVolumeCalculation >= 0 ? currentPlayerVolumeCalculation : 0;
|
|
||||||
|
|
||||||
const nextPlayerVolume =
|
|
||||||
nextPlayerVolumeCalculation <= volume ? nextPlayerVolumeCalculation : volume;
|
|
||||||
|
|
||||||
if (currentPlayer === 1) {
|
|
||||||
currentPlayerRef.current.getInternalPlayer().volume = currentPlayerVolume;
|
|
||||||
nextPlayerRef.current.getInternalPlayer().volume = nextPlayerVolume;
|
|
||||||
} else {
|
|
||||||
currentPlayerRef.current.getInternalPlayer().volume = currentPlayerVolume;
|
|
||||||
nextPlayerRef.current.getInternalPlayer().volume = nextPlayerVolume;
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
@ -5,30 +5,30 @@ import styled from 'styled-components';
|
|||||||
export type BadgeProps = MantineBadgeProps;
|
export type BadgeProps = MantineBadgeProps;
|
||||||
|
|
||||||
const StyledBadge = styled(MantineBadge)<BadgeProps>`
|
const StyledBadge = styled(MantineBadge)<BadgeProps>`
|
||||||
border-radius: var(--badge-radius);
|
border-radius: var(--badge-radius);
|
||||||
|
|
||||||
.mantine-Badge-root {
|
.mantine-Badge-root {
|
||||||
color: var(--badge-fg);
|
color: var(--badge-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mantine-Badge-inner {
|
.mantine-Badge-inner {
|
||||||
color: var(--badge-fg);
|
color: var(--badge-fg);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const _Badge = ({ children, ...props }: BadgeProps) => {
|
const _Badge = ({ children, ...props }: BadgeProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledBadge
|
<StyledBadge
|
||||||
radius="md"
|
radius="md"
|
||||||
size="sm"
|
size="sm"
|
||||||
styles={{
|
styles={{
|
||||||
root: { background: 'var(--badge-bg)' },
|
root: { background: 'var(--badge-bg)' },
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledBadge>
|
</StyledBadge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Badge = createPolymorphicComponent<'button', BadgeProps>(_Badge);
|
export const Badge = createPolymorphicComponent<'button', BadgeProps>(_Badge);
|
||||||
|
@ -8,177 +8,177 @@ import { Spinner } from '/@/renderer/components/spinner';
|
|||||||
import { Tooltip } from '/@/renderer/components/tooltip';
|
import { Tooltip } from '/@/renderer/components/tooltip';
|
||||||
|
|
||||||
export interface ButtonProps extends MantineButtonProps {
|
export interface ButtonProps extends MantineButtonProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
tooltip?: Omit<TooltipProps, 'children'>;
|
tooltip?: Omit<TooltipProps, 'children'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StyledButtonProps extends ButtonProps {
|
interface StyledButtonProps extends ButtonProps {
|
||||||
ref: Ref<HTMLButtonElement>;
|
ref: Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledButton = styled(MantineButton)<StyledButtonProps>`
|
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 {
|
|
||||||
transition: fill 0.2s ease-in-out;
|
|
||||||
fill: ${(props) => `var(--btn-${props.variant}-fg)`};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
||||||
|
border: ${(props) => `var(--btn-${props.variant}-border)`};
|
||||||
opacity: 0.6;
|
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;
|
||||||
|
|
||||||
&:not([data-disabled])&:hover {
|
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg-hover) !important`};
|
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg-hover)`};
|
|
||||||
border: ${(props) => `var(--btn-${props.variant}-border-hover)`};
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
transition: fill 0.2s ease-in-out;
|
||||||
|
fill: ${(props) => `var(--btn-${props.variant}-fg)`};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:not([data-disabled])&:focus-visible {
|
&:disabled {
|
||||||
color: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
||||||
background: ${(props) => `var(--btn-${props.variant}-bg-hover)`};
|
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
opacity: 0.6;
|
||||||
transform: none;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Button-centerLoader {
|
&:not([data-disabled])&:hover {
|
||||||
display: none;
|
color: ${(props) => `var(--btn-${props.variant}-fg-hover) !important`};
|
||||||
}
|
background: ${(props) => `var(--btn-${props.variant}-bg-hover)`};
|
||||||
|
border: ${(props) => `var(--btn-${props.variant}-border-hover)`};
|
||||||
|
|
||||||
& .mantine-Button-leftIcon {
|
svg {
|
||||||
display: flex;
|
fill: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
||||||
height: 100%;
|
}
|
||||||
margin-right: 0.5rem;
|
}
|
||||||
transform: translateY(-0.1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mantine-Button-rightIcon {
|
&:not([data-disabled])&:focus-visible {
|
||||||
display: flex;
|
color: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
||||||
margin-left: 0.5rem;
|
background: ${(props) => `var(--btn-${props.variant}-bg-hover)`};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .mantine-Button-centerLoader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .mantine-Button-leftIcon {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
transform: translateY(-0.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mantine-Button-rightIcon {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ButtonChildWrapper = styled.span<{ $loading?: boolean }>`
|
const ButtonChildWrapper = styled.span<{ $loading?: boolean }>`
|
||||||
color: ${(props) => props.$loading && 'transparent !important'};
|
color: ${(props) => props.$loading && 'transparent !important'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SpinnerWrapper = styled.div`
|
const SpinnerWrapper = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate3d(-50%, -50%, 0);
|
transform: translate3d(-50%, -50%, 0);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ children, tooltip, ...props }: ButtonProps, ref) => {
|
({ children, tooltip, ...props }: ButtonProps, ref) => {
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
withinPortal
|
withinPortal
|
||||||
{...tooltip}
|
{...tooltip}
|
||||||
>
|
>
|
||||||
<StyledButton
|
<StyledButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
loaderPosition="center"
|
loaderPosition="center"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
||||||
{props.loading && (
|
{props.loading && (
|
||||||
<SpinnerWrapper>
|
<SpinnerWrapper>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</SpinnerWrapper>
|
</SpinnerWrapper>
|
||||||
)}
|
)}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
loaderPosition="center"
|
loaderPosition="center"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
||||||
{props.loading && (
|
{props.loading && (
|
||||||
<SpinnerWrapper>
|
<SpinnerWrapper>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</SpinnerWrapper>
|
</SpinnerWrapper>
|
||||||
)}
|
)}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Button = createPolymorphicComponent<'button', ButtonProps>(_Button);
|
export const Button = createPolymorphicComponent<'button', ButtonProps>(_Button);
|
||||||
|
|
||||||
_Button.defaultProps = {
|
_Button.defaultProps = {
|
||||||
loading: undefined,
|
loading: undefined,
|
||||||
onClick: undefined,
|
onClick: undefined,
|
||||||
tooltip: undefined,
|
tooltip: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface HoldButtonProps extends ButtonProps {
|
interface HoldButtonProps extends ButtonProps {
|
||||||
timeoutProps: {
|
timeoutProps: {
|
||||||
callback: () => void;
|
callback: () => void;
|
||||||
duration: number;
|
duration: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
|
export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
|
||||||
const [, setTimeoutRemaining] = useState(timeoutProps.duration);
|
const [, setTimeoutRemaining] = useState(timeoutProps.duration);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const intervalRef = useRef(0);
|
const intervalRef = useRef(0);
|
||||||
|
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
timeoutProps.callback();
|
timeoutProps.callback();
|
||||||
setTimeoutRemaining(timeoutProps.duration);
|
setTimeoutRemaining(timeoutProps.duration);
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { start, clear } = useTimeout(callback, timeoutProps.duration);
|
const { start, clear } = useTimeout(callback, timeoutProps.duration);
|
||||||
|
|
||||||
const startTimeout = useCallback(() => {
|
const startTimeout = useCallback(() => {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
clear();
|
clear();
|
||||||
} else {
|
} else {
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
start();
|
start();
|
||||||
|
|
||||||
const intervalId = window.setInterval(() => {
|
const intervalId = window.setInterval(() => {
|
||||||
setTimeoutRemaining((prev) => prev - 100);
|
setTimeoutRemaining((prev) => prev - 100);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
intervalRef.current = intervalId;
|
intervalRef.current = intervalId;
|
||||||
}
|
}
|
||||||
}, [clear, isRunning, start]);
|
}, [clear, isRunning, start]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
sx={{ color: 'var(--danger-color)' }}
|
sx={{ color: 'var(--danger-color)' }}
|
||||||
onClick={startTimeout}
|
onClick={startTimeout}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isRunning ? 'Cancel' : props.children}
|
{isRunning ? 'Cancel' : props.children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,208 +11,208 @@ import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
|||||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||||
|
|
||||||
const CardWrapper = styled.div<{
|
const CardWrapper = styled.div<{
|
||||||
link?: boolean;
|
link?: boolean;
|
||||||
}>`
|
}>`
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: var(--card-default-bg);
|
background: var(--card-default-bg);
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
cursor: ${({ link }) => link && 'pointer'};
|
cursor: ${({ link }) => link && 'pointer'};
|
||||||
transition: border 0.2s ease-in-out, background 0.2s ease-in-out;
|
transition: border 0.2s ease-in-out, background 0.2s ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--card-default-bg-hover);
|
background: var(--card-default-bg-hover);
|
||||||
}
|
|
||||||
|
|
||||||
&:hover div {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover * {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
&:hover div {
|
||||||
outline: 1px solid #fff;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover * {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 1px solid #fff;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCard = styled.div`
|
const StyledCard = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageSection = styled.div`
|
const ImageSection = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
content: '';
|
content: '';
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
const Image = styled(SimpleImg)`
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 20%);
|
box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 20%);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ControlsContainer = styled.div`
|
const ControlsContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DetailSection = styled.div`
|
const DetailSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Row = styled.div<{ $secondary?: boolean }>`
|
const Row = styled.div<{ $secondary?: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
interface BaseGridCardProps {
|
||||||
controls: {
|
controls: {
|
||||||
cardRows: CardRow<Album | Artist | AlbumArtist>[];
|
cardRows: CardRow<Album | Artist | AlbumArtist>[];
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
playButtonBehavior: Play;
|
playButtonBehavior: Play;
|
||||||
route: CardRoute;
|
route: CardRoute;
|
||||||
};
|
};
|
||||||
data: any;
|
data: any;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumCard = ({
|
export const AlbumCard = ({
|
||||||
loading,
|
loading,
|
||||||
size,
|
size,
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
data,
|
data,
|
||||||
controls,
|
controls,
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { itemType, cardRows, route } = controls;
|
const { itemType, cardRows, route } = controls;
|
||||||
|
|
||||||
const handleNavigate = useCallback(() => {
|
const handleNavigate = useCallback(() => {
|
||||||
navigate(
|
navigate(
|
||||||
generatePath(
|
generatePath(
|
||||||
route.route,
|
route.route,
|
||||||
route.slugs?.reduce((acc, slug) => {
|
route.slugs?.reduce((acc, slug) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, [data, navigate, route.route, route.slugs]);
|
}, [data, navigate, route.route, route.slugs]);
|
||||||
|
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
return (
|
return (
|
||||||
<CardWrapper
|
<CardWrapper
|
||||||
link
|
link
|
||||||
onClick={handleNavigate}
|
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
|
|
||||||
visible
|
|
||||||
height={size}
|
|
||||||
radius="sm"
|
|
||||||
width={size}
|
|
||||||
>
|
|
||||||
<ImageSection />
|
|
||||||
</Skeleton>
|
|
||||||
<DetailSection style={{ width: '100%' }}>
|
|
||||||
{cardRows.map((_row: CardRow<Album>, index: number) => (
|
|
||||||
<Skeleton
|
|
||||||
visible
|
|
||||||
height={15}
|
|
||||||
my={3}
|
|
||||||
radius="md"
|
|
||||||
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
|
||||||
>
|
>
|
||||||
<Row />
|
<StyledCard>
|
||||||
</Skeleton>
|
<ImageSection>
|
||||||
))}
|
{data?.imageUrl ? (
|
||||||
</DetailSection>
|
<Image
|
||||||
</StyledCard>
|
animationDuration={0.3}
|
||||||
</CardWrapper>
|
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
|
||||||
|
visible
|
||||||
|
height={size}
|
||||||
|
radius="sm"
|
||||||
|
width={size}
|
||||||
|
>
|
||||||
|
<ImageSection />
|
||||||
|
</Skeleton>
|
||||||
|
<DetailSection style={{ width: '100%' }}>
|
||||||
|
{cardRows.map((_row: CardRow<Album>, index: number) => (
|
||||||
|
<Skeleton
|
||||||
|
visible
|
||||||
|
height={15}
|
||||||
|
my={3}
|
||||||
|
radius="md"
|
||||||
|
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
||||||
|
>
|
||||||
|
<Row />
|
||||||
|
</Skeleton>
|
||||||
|
))}
|
||||||
|
</DetailSection>
|
||||||
|
</StyledCard>
|
||||||
|
</CardWrapper>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,69 +11,69 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
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 {
|
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';
|
||||||
|
|
||||||
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
||||||
|
|
||||||
const PlayButton = styled.button<PlayButtonType>`
|
const PlayButton = styled.button<PlayButtonType>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: rgb(255, 255, 255);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
transition: scale 0.2s linear;
|
transition: scale 0.2s linear;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1.1;
|
scale: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1;
|
scale: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: rgb(0, 0, 0);
|
fill: rgb(0, 0, 0);
|
||||||
stroke: rgb(0, 0, 0);
|
stroke: rgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SecondaryButton = styled(_Button)`
|
const SecondaryButton = styled(_Button)`
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
transition: scale 0.2s linear;
|
transition: scale 0.2s linear;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1.1;
|
scale: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1;
|
scale: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GridCardControlsContainer = styled.div`
|
const GridCardControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ControlsRow = styled.div`
|
const ControlsRow = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% / 3);
|
height: calc(100% / 3);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// const TopControls = styled(ControlsRow)`
|
// const TopControls = styled(ControlsRow)`
|
||||||
@ -91,87 +91,87 @@ const ControlsRow = styled.div`
|
|||||||
// `;
|
// `;
|
||||||
|
|
||||||
const BottomControls = styled(ControlsRow)`
|
const BottomControls = styled(ControlsRow)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CardControls = ({
|
export const CardControls = ({
|
||||||
itemData,
|
itemData,
|
||||||
itemType,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
}: {
|
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
|
||||||
itemData: any;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
}) => {
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const handlePlay = (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: [itemData.id],
|
|
||||||
type: itemType,
|
|
||||||
},
|
|
||||||
playType: playType || playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContextMenu = useHandleGeneralContextMenu(
|
|
||||||
itemType,
|
itemType,
|
||||||
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
|
handlePlayQueueAdd,
|
||||||
);
|
}: {
|
||||||
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
|
itemData: any;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => {
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
return (
|
const handlePlay = (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||||
<GridCardControlsContainer>
|
e.preventDefault();
|
||||||
<BottomControls>
|
e.stopPropagation();
|
||||||
<PlayButton onClick={handlePlay}>
|
handlePlayQueueAdd?.({
|
||||||
<RiPlayFill size={25} />
|
byItemType: {
|
||||||
</PlayButton>
|
id: [itemData.id],
|
||||||
<Group spacing="xs">
|
type: itemType,
|
||||||
<SecondaryButton
|
},
|
||||||
disabled
|
playType: playType || playButtonBehavior,
|
||||||
p={5}
|
});
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
};
|
||||||
variant="subtle"
|
|
||||||
>
|
const handleContextMenu = useHandleGeneralContextMenu(
|
||||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
itemType,
|
||||||
{itemData?.isFavorite ? (
|
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
|
||||||
<RiHeartFill size={20} />
|
);
|
||||||
) : (
|
|
||||||
<RiHeartLine
|
return (
|
||||||
color="white"
|
<GridCardControlsContainer>
|
||||||
size={20}
|
<BottomControls>
|
||||||
/>
|
<PlayButton onClick={handlePlay}>
|
||||||
)}
|
<RiPlayFill size={25} />
|
||||||
</FavoriteWrapper>
|
</PlayButton>
|
||||||
</SecondaryButton>
|
<Group spacing="xs">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
p={5}
|
disabled
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
p={5}
|
||||||
variant="subtle"
|
sx={{ svg: { fill: 'white !important' } }}
|
||||||
onClick={(e) => {
|
variant="subtle"
|
||||||
e.preventDefault();
|
>
|
||||||
e.stopPropagation();
|
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
||||||
handleContextMenu(e, [itemData]);
|
{itemData?.isFavorite ? (
|
||||||
}}
|
<RiHeartFill size={20} />
|
||||||
>
|
) : (
|
||||||
<RiMore2Fill
|
<RiHeartLine
|
||||||
color="white"
|
color="white"
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
</SecondaryButton>
|
)}
|
||||||
</Group>
|
</FavoriteWrapper>
|
||||||
</BottomControls>
|
</SecondaryButton>
|
||||||
</GridCardControlsContainer>
|
<SecondaryButton
|
||||||
);
|
p={5}
|
||||||
|
sx={{ svg: { fill: 'white !important' } }}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleContextMenu(e, [itemData]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiMore2Fill
|
||||||
|
color="white"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</SecondaryButton>
|
||||||
|
</Group>
|
||||||
|
</BottomControls>
|
||||||
|
</GridCardControlsContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,205 +8,208 @@ import { AppRoute } from '/@/renderer/router/routes';
|
|||||||
import { CardRow } from '/@/renderer/types';
|
import { CardRow } from '/@/renderer/types';
|
||||||
|
|
||||||
const Row = styled.div<{ $secondary?: boolean }>`
|
const Row = styled.div<{ $secondary?: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface CardRowsProps {
|
interface CardRowsProps {
|
||||||
data: any;
|
data: any;
|
||||||
rows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
rows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rows.map((row, index: number) => {
|
{rows.map((row, index: number) => {
|
||||||
if (row.arrayProperty && row.route) {
|
if (row.arrayProperty && row.route) {
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
key={`row-${row.property}-${index}`}
|
key={`row-${row.property}-${index}`}
|
||||||
$secondary={index > 0}
|
$secondary={index > 0}
|
||||||
>
|
>
|
||||||
{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
|
$noSelect
|
||||||
$secondary
|
$secondary
|
||||||
sx={{
|
sx={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
padding: '0 2px 0 1px',
|
padding: '0 2px 0 1px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
,
|
,
|
||||||
</Text>
|
</Text>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<Text
|
<Text
|
||||||
$link
|
$link
|
||||||
$noSelect
|
$noSelect
|
||||||
$secondary={index > 0}
|
$secondary={index > 0}
|
||||||
component={Link}
|
component={Link}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
to={generatePath(
|
to={generatePath(
|
||||||
row.route!.route,
|
row.route!.route,
|
||||||
row.route!.slugs?.reduce((acc, slug) => {
|
row.route!.slugs?.reduce((acc, slug) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[slug.slugProperty]: data[row.property][itemIndex][slug.idProperty],
|
[slug.slugProperty]:
|
||||||
};
|
data[row.property][itemIndex][
|
||||||
}, {}),
|
slug.idProperty
|
||||||
)}
|
],
|
||||||
onClick={(e) => e.stopPropagation()}
|
};
|
||||||
>
|
}, {}),
|
||||||
{row.arrayProperty && item[row.arrayProperty]}
|
)}
|
||||||
</Text>
|
onClick={(e) => e.stopPropagation()}
|
||||||
</React.Fragment>
|
>
|
||||||
))}
|
{row.arrayProperty && item[row.arrayProperty]}
|
||||||
</Row>
|
</Text>
|
||||||
);
|
</React.Fragment>
|
||||||
}
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (row.arrayProperty) {
|
if (row.arrayProperty) {
|
||||||
return (
|
return (
|
||||||
<Row key={`row-${row.property}`}>
|
<Row key={`row-${row.property}`}>
|
||||||
{data[row.property].map((item: any) => (
|
{data[row.property].map((item: any) => (
|
||||||
<Text
|
<Text
|
||||||
key={`${data.id}-${item.id}`}
|
key={`${data.id}-${item.id}`}
|
||||||
$noSelect
|
$noSelect
|
||||||
$secondary={index > 0}
|
$secondary={index > 0}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
>
|
>
|
||||||
{row.arrayProperty && item[row.arrayProperty]}
|
{row.arrayProperty && item[row.arrayProperty]}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row key={`row-${row.property}`}>
|
<Row key={`row-${row.property}`}>
|
||||||
{row.route ? (
|
{row.route ? (
|
||||||
<Text
|
<Text
|
||||||
$link
|
$link
|
||||||
$noSelect
|
$noSelect
|
||||||
component={Link}
|
component={Link}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
to={generatePath(
|
to={generatePath(
|
||||||
row.route.route,
|
row.route.route,
|
||||||
row.route.slugs?.reduce((acc, slug) => {
|
row.route.slugs?.reduce((acc, slug) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{data && data[row.property]}
|
{data && data[row.property]}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
$noSelect
|
$noSelect
|
||||||
$secondary={index > 0}
|
$secondary={index > 0}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
>
|
>
|
||||||
{data && data[row.property]}
|
{data && data[row.property]}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
|
export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
|
||||||
albumArtists: {
|
albumArtists: {
|
||||||
arrayProperty: 'name',
|
arrayProperty: 'name',
|
||||||
property: 'albumArtists',
|
property: 'albumArtists',
|
||||||
route: {
|
route: {
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
artists: {
|
||||||
artists: {
|
arrayProperty: 'name',
|
||||||
arrayProperty: 'name',
|
property: 'artists',
|
||||||
property: 'artists',
|
route: {
|
||||||
route: {
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
},
|
||||||
},
|
},
|
||||||
},
|
createdAt: {
|
||||||
createdAt: {
|
property: 'createdAt',
|
||||||
property: 'createdAt',
|
},
|
||||||
},
|
duration: {
|
||||||
duration: {
|
property: 'duration',
|
||||||
property: 'duration',
|
},
|
||||||
},
|
lastPlayedAt: {
|
||||||
lastPlayedAt: {
|
property: 'lastPlayedAt',
|
||||||
property: 'lastPlayedAt',
|
},
|
||||||
},
|
name: {
|
||||||
name: {
|
property: 'name',
|
||||||
property: 'name',
|
route: {
|
||||||
route: {
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
},
|
||||||
|
},
|
||||||
|
playCount: {
|
||||||
|
property: 'playCount',
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
property: 'userRating',
|
||||||
|
},
|
||||||
|
releaseDate: {
|
||||||
|
property: 'releaseDate',
|
||||||
|
},
|
||||||
|
releaseYear: {
|
||||||
|
property: 'releaseYear',
|
||||||
|
},
|
||||||
|
songCount: {
|
||||||
|
property: 'songCount',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
playCount: {
|
|
||||||
property: 'playCount',
|
|
||||||
},
|
|
||||||
rating: {
|
|
||||||
property: 'userRating',
|
|
||||||
},
|
|
||||||
releaseDate: {
|
|
||||||
property: 'releaseDate',
|
|
||||||
},
|
|
||||||
releaseYear: {
|
|
||||||
property: 'releaseYear',
|
|
||||||
},
|
|
||||||
songCount: {
|
|
||||||
property: 'songCount',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
||||||
albumCount: {
|
albumCount: {
|
||||||
property: 'albumCount',
|
property: 'albumCount',
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
property: 'duration',
|
property: 'duration',
|
||||||
},
|
},
|
||||||
genres: {
|
genres: {
|
||||||
property: 'genres',
|
property: 'genres',
|
||||||
},
|
},
|
||||||
lastPlayedAt: {
|
lastPlayedAt: {
|
||||||
property: 'lastPlayedAt',
|
property: 'lastPlayedAt',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
property: 'name',
|
property: 'name',
|
||||||
route: {
|
route: {
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
playCount: {
|
||||||
|
property: 'playCount',
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
property: 'userRating',
|
||||||
|
},
|
||||||
|
songCount: {
|
||||||
|
property: 'songCount',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
playCount: {
|
|
||||||
property: 'playCount',
|
|
||||||
},
|
|
||||||
rating: {
|
|
||||||
property: 'userRating',
|
|
||||||
},
|
|
||||||
songCount: {
|
|
||||||
property: 'songCount',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -10,197 +10,197 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
|
|||||||
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
interface BaseGridCardProps {
|
||||||
controls: {
|
controls: {
|
||||||
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||||
handleFavorite: (options: {
|
handleFavorite: (options: {
|
||||||
id: string[];
|
id: string[];
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
handlePlayQueueAdd: ((options: PlayQueueAddOptions) => void) | undefined;
|
handlePlayQueueAdd: ((options: PlayQueueAddOptions) => void) | undefined;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
playButtonBehavior: Play;
|
playButtonBehavior: Play;
|
||||||
route: CardRoute;
|
route: CardRoute;
|
||||||
};
|
};
|
||||||
data: any;
|
data: any;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
.card-controls {
|
.card-controls {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageContainerStyles = css`
|
const ImageContainerStyles = css`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--card-default-bg);
|
background: var(--card-default-bg);
|
||||||
border-radius: var(--card-poster-radius);
|
border-radius: var(--card-poster-radius);
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
content: '';
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::before {
|
&::before {
|
||||||
opacity: 0.5;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
content: '';
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .card-controls {
|
&:hover {
|
||||||
opacity: 1;
|
&::before {
|
||||||
}
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageContainer = styled(Link)<{ $isFavorite?: boolean }>`
|
const ImageContainer = styled(Link)<{ $isFavorite?: boolean }>`
|
||||||
${ImageContainerStyles}
|
${ImageContainerStyles}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageContainerSkeleton = styled.div`
|
const ImageContainerSkeleton = styled.div`
|
||||||
${ImageContainerStyles}
|
${ImageContainerStyles}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
const Image = styled(SimpleImg)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DetailContainer = styled.div`
|
const DetailContainer = styled.div`
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PosterCard = ({
|
export const PosterCard = ({
|
||||||
data,
|
data,
|
||||||
controls,
|
controls,
|
||||||
isLoading,
|
isLoading,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
}: BaseGridCardProps & { uniqueId: string }) => {
|
}: BaseGridCardProps & { uniqueId: string }) => {
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
const path = generatePath(
|
const path = generatePath(
|
||||||
controls.route.route,
|
controls.route.route,
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Placeholder = RiAlbumFill;
|
let Placeholder = RiAlbumFill;
|
||||||
|
|
||||||
switch (controls.itemType) {
|
switch (controls.itemType) {
|
||||||
case LibraryItem.ALBUM:
|
case LibraryItem.ALBUM:
|
||||||
Placeholder = RiAlbumFill;
|
Placeholder = RiAlbumFill;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ARTIST:
|
case LibraryItem.ARTIST:
|
||||||
Placeholder = RiUserVoiceFill;
|
Placeholder = RiUserVoiceFill;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
Placeholder = RiUserVoiceFill;
|
Placeholder = RiUserVoiceFill;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.PLAYLIST:
|
case LibraryItem.PLAYLIST:
|
||||||
Placeholder = RiPlayListFill;
|
Placeholder = RiPlayListFill;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Placeholder = RiAlbumFill;
|
Placeholder = RiAlbumFill;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PosterCardContainer key={`${uniqueId}-${data.id}`}>
|
||||||
|
<ImageContainer
|
||||||
|
$isFavorite={data?.userFavorite}
|
||||||
|
to={path}
|
||||||
|
>
|
||||||
|
{data?.imageUrl ? (
|
||||||
|
<Image
|
||||||
|
importance="auto"
|
||||||
|
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
|
||||||
|
handleFavorite={controls.handleFavorite}
|
||||||
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
itemData={data}
|
||||||
|
itemType={controls.itemType}
|
||||||
|
/>
|
||||||
|
</ImageContainer>
|
||||||
|
<DetailContainer>
|
||||||
|
<CardRows
|
||||||
|
data={data}
|
||||||
|
rows={controls.cardRows}
|
||||||
|
/>
|
||||||
|
</DetailContainer>
|
||||||
|
</PosterCardContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PosterCardContainer key={`${uniqueId}-${data.id}`}>
|
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
|
||||||
<ImageContainer
|
|
||||||
$isFavorite={data?.userFavorite}
|
|
||||||
to={path}
|
|
||||||
>
|
|
||||||
{data?.imageUrl ? (
|
|
||||||
<Image
|
|
||||||
importance="auto"
|
|
||||||
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
|
|
||||||
handleFavorite={controls.handleFavorite}
|
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
|
||||||
itemData={data}
|
|
||||||
itemType={controls.itemType}
|
|
||||||
/>
|
|
||||||
</ImageContainer>
|
|
||||||
<DetailContainer>
|
|
||||||
<CardRows
|
|
||||||
data={data}
|
|
||||||
rows={controls.cardRows}
|
|
||||||
/>
|
|
||||||
</DetailContainer>
|
|
||||||
</PosterCardContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
|
|
||||||
<Skeleton
|
|
||||||
visible
|
|
||||||
radius="sm"
|
|
||||||
>
|
|
||||||
<ImageContainerSkeleton />
|
|
||||||
</Skeleton>
|
|
||||||
<DetailContainer>
|
|
||||||
<Stack spacing="sm">
|
|
||||||
{controls.cardRows.map((row, index) => (
|
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={`${index}-${row.arrayProperty}`}
|
visible
|
||||||
visible
|
radius="sm"
|
||||||
height={14}
|
>
|
||||||
radius="sm"
|
<ImageContainerSkeleton />
|
||||||
/>
|
</Skeleton>
|
||||||
))}
|
<DetailContainer>
|
||||||
</Stack>
|
<Stack spacing="sm">
|
||||||
</DetailContainer>
|
{controls.cardRows.map((row, index) => (
|
||||||
</PosterCardContainer>
|
<Skeleton
|
||||||
);
|
key={`${index}-${row.arrayProperty}`}
|
||||||
|
visible
|
||||||
|
height={14}
|
||||||
|
radius="sm"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</DetailContainer>
|
||||||
|
</PosterCardContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,30 +3,30 @@ import { Checkbox as MantineCheckbox, CheckboxProps } from '@mantine/core';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledCheckbox = styled(MantineCheckbox)`
|
const StyledCheckbox = styled(MantineCheckbox)`
|
||||||
& .mantine-Checkbox-input {
|
& .mantine-Checkbox-input {
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:checked) {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(:checked) {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
({ ...props }: CheckboxProps, ref) => {
|
({ ...props }: CheckboxProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -4,124 +4,124 @@ import { motion, Variants } from 'framer-motion';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface ContextMenuProps {
|
interface ContextMenuProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
xPos: number;
|
xPos: number;
|
||||||
yPos: number;
|
yPos: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContextMenuContainer = styled(motion.div)<Omit<ContextMenuProps, 'children'>>`
|
const ContextMenuContainer = styled(motion.div)<Omit<ContextMenuProps, 'children'>>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${({ yPos }) => yPos}px !important;
|
top: ${({ yPos }) => yPos}px !important;
|
||||||
left: ${({ xPos }) => xPos}px !important;
|
left: ${({ xPos }) => xPos}px !important;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
min-width: ${({ minWidth }) => minWidth}px;
|
min-width: ${({ minWidth }) => minWidth}px;
|
||||||
max-width: ${({ maxWidth }) => maxWidth}px;
|
max-width: ${({ maxWidth }) => maxWidth}px;
|
||||||
background: var(--dropdown-menu-bg);
|
background: var(--dropdown-menu-bg);
|
||||||
border-radius: var(--dropdown-menu-border-radius);
|
border-radius: var(--dropdown-menu-border-radius);
|
||||||
box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 40%);
|
box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 40%);
|
||||||
|
|
||||||
button:first-child {
|
button:first-child {
|
||||||
border-top-left-radius: var(--dropdown-menu-border-radius);
|
border-top-left-radius: var(--dropdown-menu-border-radius);
|
||||||
border-top-right-radius: var(--dropdown-menu-border-radius);
|
border-top-right-radius: var(--dropdown-menu-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:last-child {
|
button:last-child {
|
||||||
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
||||||
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledContextMenuButton = styled(UnstyledButton)`
|
export const StyledContextMenuButton = styled(UnstyledButton)`
|
||||||
padding: var(--dropdown-menu-item-padding);
|
padding: var(--dropdown-menu-item-padding);
|
||||||
color: var(--dropdown-menu-fg);
|
color: var(--dropdown-menu-fg);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: var(--content-font-family);
|
font-family: var(--content-font-family);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: var(--dropdown-menu-bg);
|
background: var(--dropdown-menu-bg);
|
||||||
border: none;
|
border: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
& .mantine-Button-inner {
|
& .mantine-Button-inner {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--dropdown-menu-bg-hover);
|
background: var(--dropdown-menu-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ContextMenuButton = forwardRef(
|
export const ContextMenuButton = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
rightIcon,
|
rightIcon,
|
||||||
leftIcon,
|
leftIcon,
|
||||||
...props
|
...props
|
||||||
}: UnstyledButtonProps &
|
}: UnstyledButtonProps &
|
||||||
React.ComponentPropsWithoutRef<'button'> & {
|
React.ComponentPropsWithoutRef<'button'> & {
|
||||||
leftIcon?: ReactNode;
|
leftIcon?: ReactNode;
|
||||||
rightIcon?: ReactNode;
|
rightIcon?: ReactNode;
|
||||||
},
|
},
|
||||||
ref: any,
|
ref: any,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<StyledContextMenuButton
|
<StyledContextMenuButton
|
||||||
{...props}
|
{...props}
|
||||||
key={props.key}
|
key={props.key}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
as="button"
|
as="button"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group spacing="md">
|
<Group spacing="md">
|
||||||
<Box>{leftIcon}</Box>
|
<Box>{leftIcon}</Box>
|
||||||
<Box mr="2rem">{children}</Box>
|
<Box mr="2rem">{children}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
<Box>{rightIcon}</Box>
|
<Box>{rightIcon}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
</StyledContextMenuButton>
|
</StyledContextMenuButton>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
closed: {
|
closed: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.1,
|
duration: 0.1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
open: {
|
||||||
open: {
|
opacity: 1,
|
||||||
opacity: 1,
|
transition: {
|
||||||
transition: {
|
duration: 0.1,
|
||||||
duration: 0.1,
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ContextMenu = forwardRef(
|
export const ContextMenu = forwardRef(
|
||||||
({ yPos, xPos, minWidth, maxWidth, children }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
({ yPos, xPos, minWidth, maxWidth, children }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
||||||
return (
|
return (
|
||||||
<ContextMenuContainer
|
<ContextMenuContainer
|
||||||
ref={ref}
|
ref={ref}
|
||||||
animate="open"
|
animate="open"
|
||||||
initial="closed"
|
initial="closed"
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
minWidth={minWidth}
|
minWidth={minWidth}
|
||||||
variants={variants}
|
variants={variants}
|
||||||
xPos={xPos}
|
xPos={xPos}
|
||||||
yPos={yPos}
|
yPos={yPos}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuContainer>
|
</ContextMenuContainer>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -3,47 +3,47 @@ import { DatePicker as MantineDatePicker } from '@mantine/dates';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface DatePickerProps extends MantineDatePickerProps {
|
interface DatePickerProps extends MantineDatePickerProps {
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledDatePicker = styled(MantineDatePicker)<DatePickerProps>`
|
const StyledDatePicker = styled(MantineDatePicker)<DatePickerProps>`
|
||||||
& .mantine-DatePicker-input {
|
& .mantine-DatePicker-input {
|
||||||
color: var(--input-fg);
|
color: var(--input-fg);
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-DatePicker-icon {
|
& .mantine-DatePicker-icon {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-DatePicker-required {
|
& .mantine-DatePicker-required {
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-DatePicker-label {
|
& .mantine-DatePicker-label {
|
||||||
font-family: var(--label-font-family);
|
font-family: var(--label-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-DateRangePicker-disabled {
|
& .mantine-DateRangePicker-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DatePicker = ({ width, maxWidth, ...props }: DatePickerProps) => {
|
export const DatePicker = ({ width, maxWidth, ...props }: DatePickerProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledDatePicker
|
<StyledDatePicker
|
||||||
{...props}
|
{...props}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DatePicker.defaultProps = {
|
DatePicker.defaultProps = {
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
MenuProps as MantineMenuProps,
|
MenuProps as MantineMenuProps,
|
||||||
MenuItemProps as MantineMenuItemProps,
|
MenuItemProps as MantineMenuItemProps,
|
||||||
MenuLabelProps as MantineMenuLabelProps,
|
MenuLabelProps as MantineMenuLabelProps,
|
||||||
MenuDividerProps as MantineMenuDividerProps,
|
MenuDividerProps as MantineMenuDividerProps,
|
||||||
MenuDropdownProps as MantineMenuDropdownProps,
|
MenuDropdownProps as MantineMenuDropdownProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Menu as MantineMenu, createPolymorphicComponent } from '@mantine/core';
|
import { Menu as MantineMenu, createPolymorphicComponent } from '@mantine/core';
|
||||||
import { RiArrowLeftSFill } from 'react-icons/ri';
|
import { RiArrowLeftSFill } from 'react-icons/ri';
|
||||||
@ -12,9 +12,9 @@ import styled from 'styled-components';
|
|||||||
type MenuProps = MantineMenuProps;
|
type MenuProps = MantineMenuProps;
|
||||||
type MenuLabelProps = MantineMenuLabelProps;
|
type MenuLabelProps = MantineMenuLabelProps;
|
||||||
interface MenuItemProps extends MantineMenuItemProps {
|
interface MenuItemProps extends MantineMenuItemProps {
|
||||||
$danger?: boolean;
|
$danger?: boolean;
|
||||||
$isActive?: boolean;
|
$isActive?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
type MenuDividerProps = MantineMenuDividerProps;
|
type MenuDividerProps = MantineMenuDividerProps;
|
||||||
type MenuDropdownProps = MantineMenuDropdownProps;
|
type MenuDropdownProps = MantineMenuDropdownProps;
|
||||||
@ -22,46 +22,46 @@ type MenuDropdownProps = MantineMenuDropdownProps;
|
|||||||
const StyledMenu = styled(MantineMenu)<MenuProps>``;
|
const StyledMenu = styled(MantineMenu)<MenuProps>``;
|
||||||
|
|
||||||
const StyledMenuLabel = styled(MantineMenu.Label)<MenuLabelProps>`
|
const StyledMenuLabel = styled(MantineMenu.Label)<MenuLabelProps>`
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-family: var(--content-font-family);
|
font-family: var(--content-font-family);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledMenuItem = styled(MantineMenu.Item)<MenuItemProps>`
|
const StyledMenuItem = styled(MantineMenu.Item)<MenuItemProps>`
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--dropdown-menu-item-padding);
|
padding: var(--dropdown-menu-item-padding);
|
||||||
font-size: var(--dropdown-menu-item-font-size);
|
font-size: var(--dropdown-menu-item-font-size);
|
||||||
font-family: var(--content-font-family);
|
font-family: var(--content-font-family);
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--dropdown-menu-bg-hover);
|
background-color: var(--dropdown-menu-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Menu-itemLabel {
|
& .mantine-Menu-itemLabel {
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
color: ${(props) => (props.$danger ? 'var(--danger-color)' : 'var(--dropdown-menu-fg)')};
|
color: ${(props) => (props.$danger ? 'var(--danger-color)' : 'var(--dropdown-menu-fg)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Menu-itemRightSection {
|
& .mantine-Menu-itemRightSection {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--dropdown-menu-bg);
|
background: var(--dropdown-menu-bg);
|
||||||
border: var(--dropdown-menu-border);
|
border: var(--dropdown-menu-border);
|
||||||
border-radius: var(--dropdown-menu-border-radius);
|
border-radius: var(--dropdown-menu-border-radius);
|
||||||
filter: drop-shadow(0 0 5px rgb(0, 0, 0, 50%));
|
filter: drop-shadow(0 0 5px rgb(0, 0, 0, 50%));
|
||||||
|
|
||||||
/* *:first-child {
|
/* *:first-child {
|
||||||
border-top-left-radius: var(--dropdown-menu-border-radius);
|
border-top-left-radius: var(--dropdown-menu-border-radius);
|
||||||
border-top-right-radius: var(--dropdown-menu-border-radius);
|
border-top-right-radius: var(--dropdown-menu-border-radius);
|
||||||
}
|
}
|
||||||
@ -73,51 +73,51 @@ const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledMenuDivider = styled(MantineMenu.Divider)`
|
const StyledMenuDivider = styled(MantineMenu.Divider)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledMenu
|
<StyledMenu
|
||||||
withinPortal
|
withinPortal
|
||||||
styles={{
|
styles={{
|
||||||
dropdown: {
|
dropdown: {
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledMenu>
|
</StyledMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
|
const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
|
||||||
return <StyledMenuLabel {...props}>{children}</StyledMenuLabel>;
|
return <StyledMenuLabel {...props}>{children}</StyledMenuLabel>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pMenuItem = ({ $isActive, $danger, children, ...props }: MenuItemProps) => {
|
const pMenuItem = ({ $isActive, $danger, children, ...props }: MenuItemProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledMenuItem
|
<StyledMenuItem
|
||||||
$danger={$danger}
|
$danger={$danger}
|
||||||
$isActive={$isActive}
|
$isActive={$isActive}
|
||||||
rightSection={$isActive && <RiArrowLeftSFill size={15} />}
|
rightSection={$isActive && <RiArrowLeftSFill size={15} />}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledMenuItem>
|
</StyledMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => {
|
const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => {
|
||||||
return <StyledMenuDropdown {...props}>{children}</StyledMenuDropdown>;
|
return <StyledMenuDropdown {...props}>{children}</StyledMenuDropdown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuItem = createPolymorphicComponent<'button', MenuItemProps>(pMenuItem);
|
const MenuItem = createPolymorphicComponent<'button', MenuItemProps>(pMenuItem);
|
||||||
|
|
||||||
const MenuDivider = ({ ...props }: MenuDividerProps) => {
|
const MenuDivider = ({ ...props }: MenuDividerProps) => {
|
||||||
return <StyledMenuDivider {...props} />;
|
return <StyledMenuDivider {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
DropdownMenu.Label = MenuLabel;
|
DropdownMenu.Label = MenuLabel;
|
||||||
|
@ -15,245 +15,247 @@ import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue
|
|||||||
import { Play } from '/@/renderer/types';
|
import { Play } from '/@/renderer/types';
|
||||||
|
|
||||||
const Carousel = styled(motion.div)`
|
const Carousel = styled(motion.div)`
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 35vh;
|
height: 35vh;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(180deg, var(--main-bg), rgba(25, 26, 28, 60%));
|
background: linear-gradient(180deg, var(--main-bg), rgba(25, 26, 28, 60%));
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Grid = styled.div`
|
const Grid = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-columns: 1fr;
|
grid-auto-columns: 1fr;
|
||||||
grid-template-areas: 'image info';
|
grid-template-areas: 'image info';
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
grid-template-columns: 200px minmax(0, 1fr);
|
grid-template-columns: 200px minmax(0, 1fr);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageColumn = styled.div`
|
const ImageColumn = styled.div`
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InfoColumn = styled.div`
|
const InfoColumn = styled.div`
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-area: info;
|
grid-area: info;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BackgroundImage = styled.img`
|
const BackgroundImage = styled.img`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
width: 150%;
|
width: 150%;
|
||||||
height: 150%;
|
height: 150%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: 0 30%;
|
object-position: 0 30%;
|
||||||
filter: blur(24px);
|
filter: blur(24px);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BackgroundImageOverlay = styled.div`
|
const BackgroundImageOverlay = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(180deg, rgba(25, 26, 28, 30%), var(--main-bg));
|
background: linear-gradient(180deg, rgba(25, 26, 28, 30%), var(--main-bg));
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled(Link)`
|
const Wrapper = styled(Link)`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TitleWrapper = styled.div`
|
const TitleWrapper = styled.div`
|
||||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
animate: {
|
animate: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { opacity: { duration: 0.5 } },
|
transition: { opacity: { duration: 0.5 } },
|
||||||
},
|
},
|
||||||
exit: {
|
exit: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: { opacity: { duration: 0.5 } },
|
transition: { opacity: { duration: 0.5 } },
|
||||||
},
|
},
|
||||||
initial: {
|
initial: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FeatureCarouselProps {
|
interface FeatureCarouselProps {
|
||||||
data: Album[] | undefined;
|
data: Album[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const [itemIndex, setItemIndex] = useState(0);
|
const [itemIndex, setItemIndex] = useState(0);
|
||||||
const [direction, setDirection] = useState(0);
|
const [direction, setDirection] = useState(0);
|
||||||
|
|
||||||
const currentItem = data?.[itemIndex];
|
const currentItem = data?.[itemIndex];
|
||||||
|
|
||||||
const handleNext = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleNext = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDirection(1);
|
setDirection(1);
|
||||||
if (itemIndex === (data?.length || 0) - 1 || 0) {
|
if (itemIndex === (data?.length || 0) - 1 || 0) {
|
||||||
setItemIndex(0);
|
setItemIndex(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setItemIndex((prev) => prev + 1);
|
setItemIndex((prev) => prev + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevious = (e: MouseEvent<HTMLButtonElement>) => {
|
const handlePrevious = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDirection(-1);
|
setDirection(-1);
|
||||||
if (itemIndex === 0) {
|
if (itemIndex === 0) {
|
||||||
setItemIndex((data?.length || 0) - 1);
|
setItemIndex((data?.length || 0) - 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setItemIndex((prev) => prev - 1);
|
setItemIndex((prev) => prev - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}>
|
<Wrapper
|
||||||
<AnimatePresence
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}
|
||||||
custom={direction}
|
>
|
||||||
initial={false}
|
<AnimatePresence
|
||||||
mode="popLayout"
|
custom={direction}
|
||||||
>
|
initial={false}
|
||||||
{data && (
|
mode="popLayout"
|
||||||
<Carousel
|
>
|
||||||
key={`image-${itemIndex}`}
|
{data && (
|
||||||
animate="animate"
|
<Carousel
|
||||||
custom={direction}
|
key={`image-${itemIndex}`}
|
||||||
exit="exit"
|
animate="animate"
|
||||||
initial="initial"
|
custom={direction}
|
||||||
variants={variants}
|
exit="exit"
|
||||||
>
|
initial="initial"
|
||||||
<Grid>
|
variants={variants}
|
||||||
<ImageColumn>
|
|
||||||
<Image
|
|
||||||
height={225}
|
|
||||||
placeholder="var(--card-default-bg)"
|
|
||||||
radius="md"
|
|
||||||
src={data[itemIndex]?.imageUrl}
|
|
||||||
sx={{ objectFit: 'cover' }}
|
|
||||||
width={225}
|
|
||||||
/>
|
|
||||||
</ImageColumn>
|
|
||||||
<InfoColumn>
|
|
||||||
<Stack
|
|
||||||
spacing="md"
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<TitleWrapper>
|
|
||||||
<TextTitle
|
|
||||||
lh="3.5rem"
|
|
||||||
order={1}
|
|
||||||
overflow="hidden"
|
|
||||||
sx={{ fontSize: '3.5rem' }}
|
|
||||||
weight={900}
|
|
||||||
>
|
>
|
||||||
{currentItem?.name}
|
<Grid>
|
||||||
</TextTitle>
|
<ImageColumn>
|
||||||
</TitleWrapper>
|
<Image
|
||||||
<TitleWrapper>
|
height={225}
|
||||||
{currentItem?.albumArtists.slice(0, 1).map((artist) => (
|
placeholder="var(--card-default-bg)"
|
||||||
<TextTitle
|
radius="md"
|
||||||
key={`carousel-artist-${artist.id}`}
|
src={data[itemIndex]?.imageUrl}
|
||||||
order={2}
|
sx={{ objectFit: 'cover' }}
|
||||||
weight={600}
|
width={225}
|
||||||
>
|
/>
|
||||||
{artist.name}
|
</ImageColumn>
|
||||||
</TextTitle>
|
<InfoColumn>
|
||||||
))}
|
<Stack
|
||||||
</TitleWrapper>
|
spacing="md"
|
||||||
<Group>
|
sx={{ width: '100%' }}
|
||||||
{currentItem?.genres?.slice(0, 1).map((genre) => (
|
>
|
||||||
<Badge
|
<TitleWrapper>
|
||||||
key={`carousel-genre-${genre.id}`}
|
<TextTitle
|
||||||
size="lg"
|
lh="3.5rem"
|
||||||
>
|
order={1}
|
||||||
{genre.name}
|
overflow="hidden"
|
||||||
</Badge>
|
sx={{ fontSize: '3.5rem' }}
|
||||||
))}
|
weight={900}
|
||||||
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
>
|
||||||
<Badge size="lg">{currentItem?.songCount} tracks</Badge>
|
{currentItem?.name}
|
||||||
</Group>
|
</TextTitle>
|
||||||
<Group position="apart">
|
</TitleWrapper>
|
||||||
<Button
|
<TitleWrapper>
|
||||||
size="lg"
|
{currentItem?.albumArtists.slice(0, 1).map((artist) => (
|
||||||
style={{ borderRadius: '5rem' }}
|
<TextTitle
|
||||||
variant="outline"
|
key={`carousel-artist-${artist.id}`}
|
||||||
onClick={(e) => {
|
order={2}
|
||||||
e.preventDefault();
|
weight={600}
|
||||||
e.stopPropagation();
|
>
|
||||||
if (!currentItem) return;
|
{artist.name}
|
||||||
|
</TextTitle>
|
||||||
|
))}
|
||||||
|
</TitleWrapper>
|
||||||
|
<Group>
|
||||||
|
{currentItem?.genres?.slice(0, 1).map((genre) => (
|
||||||
|
<Badge
|
||||||
|
key={`carousel-genre-${genre.id}`}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{genre.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
||||||
|
<Badge size="lg">{currentItem?.songCount} tracks</Badge>
|
||||||
|
</Group>
|
||||||
|
<Group position="apart">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
style={{ borderRadius: '5rem' }}
|
||||||
|
variant="outline"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!currentItem) return;
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
byItemType: {
|
byItemType: {
|
||||||
id: [currentItem.id],
|
id: [currentItem.id],
|
||||||
type: LibraryItem.ALBUM,
|
type: LibraryItem.ALBUM,
|
||||||
},
|
},
|
||||||
playType: Play.NOW,
|
playType: Play.NOW,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Play
|
Play
|
||||||
</Button>
|
</Button>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<Button
|
<Button
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handlePrevious}
|
onClick={handlePrevious}
|
||||||
>
|
>
|
||||||
<RiArrowLeftSLine size="2rem" />
|
<RiArrowLeftSLine size="2rem" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
>
|
>
|
||||||
<RiArrowRightSLine size="2rem" />
|
<RiArrowRightSLine size="2rem" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</InfoColumn>
|
</InfoColumn>
|
||||||
</Grid>
|
</Grid>
|
||||||
<BackgroundImage
|
<BackgroundImage
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={currentItem?.imageUrl || undefined}
|
src={currentItem?.imageUrl || undefined}
|
||||||
/>
|
/>
|
||||||
<BackgroundImageOverlay />
|
<BackgroundImageOverlay />
|
||||||
</Carousel>
|
</Carousel>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,262 +17,267 @@ import { usePlayButtonBehavior } from '/@/renderer/store';
|
|||||||
import { CardRoute, CardRow } from '/@/renderer/types';
|
import { CardRoute, CardRow } from '/@/renderer/types';
|
||||||
|
|
||||||
const getSlidesPerView = (windowWidth: number) => {
|
const getSlidesPerView = (windowWidth: number) => {
|
||||||
if (windowWidth < 400) return 2;
|
if (windowWidth < 400) return 2;
|
||||||
if (windowWidth < 700) return 3;
|
if (windowWidth < 700) return 3;
|
||||||
if (windowWidth < 900) return 4;
|
if (windowWidth < 900) return 4;
|
||||||
if (windowWidth < 1100) return 5;
|
if (windowWidth < 1100) return 5;
|
||||||
if (windowWidth < 1300) return 6;
|
if (windowWidth < 1300) return 6;
|
||||||
if (windowWidth < 1500) return 7;
|
if (windowWidth < 1500) return 7;
|
||||||
if (windowWidth < 1920) return 8;
|
if (windowWidth < 1920) return 8;
|
||||||
return 10;
|
return 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CarouselContainer = styled(Stack)`
|
const CarouselContainer = styled(Stack)`
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface TitleProps {
|
interface TitleProps {
|
||||||
handleNext?: () => void;
|
handleNext?: () => void;
|
||||||
handlePrev?: () => void;
|
handlePrev?: () => void;
|
||||||
label?: string | ReactNode;
|
label?: string | ReactNode;
|
||||||
pagination: {
|
pagination: {
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
hasPreviousPage: boolean;
|
hasPreviousPage: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
|
||||||
return (
|
return (
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
{isValidElement(label) ? (
|
{isValidElement(label) ? (
|
||||||
label
|
label
|
||||||
) : (
|
) : (
|
||||||
<TextTitle
|
<TextTitle
|
||||||
order={2}
|
order={2}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
disabled={!pagination.hasPreviousPage}
|
disabled={!pagination.hasPreviousPage}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handlePrev}
|
onClick={handlePrev}
|
||||||
>
|
>
|
||||||
<RiArrowLeftSLine />
|
<RiArrowLeftSLine />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
disabled={!pagination.hasNextPage}
|
disabled={!pagination.hasNextPage}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
>
|
>
|
||||||
<RiArrowRightSLine />
|
<RiArrowRightSLine />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SwiperGridCarouselProps {
|
interface SwiperGridCarouselProps {
|
||||||
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||||
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
|
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
route: CardRoute;
|
route: CardRoute;
|
||||||
swiperProps?: SwiperOptions;
|
swiperProps?: SwiperOptions;
|
||||||
title?: {
|
title?: {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
hasPagination?: boolean;
|
hasPagination?: boolean;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
label: string | ReactNode;
|
label: string | ReactNode;
|
||||||
};
|
};
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SwiperGridCarousel = ({
|
export const SwiperGridCarousel = ({
|
||||||
cardRows,
|
|
||||||
data,
|
|
||||||
itemType,
|
|
||||||
route,
|
|
||||||
swiperProps,
|
|
||||||
title,
|
|
||||||
isLoading,
|
|
||||||
uniqueId,
|
|
||||||
}: SwiperGridCarouselProps) => {
|
|
||||||
const swiperRef = useRef<SwiperCore | any>(null);
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
hasNextPage: (data?.length || 0) > Math.round(3),
|
|
||||||
hasPreviousPage: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createFavoriteMutation = useCreateFavorite({});
|
|
||||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
|
||||||
|
|
||||||
const handleFavorite = useCallback(
|
|
||||||
(options: { id: string[]; isFavorite: boolean; itemType: LibraryItem; serverId: string }) => {
|
|
||||||
const { id, itemType, isFavorite, serverId } = options;
|
|
||||||
if (isFavorite) {
|
|
||||||
deleteFavoriteMutation.mutate({
|
|
||||||
query: {
|
|
||||||
id,
|
|
||||||
type: itemType,
|
|
||||||
},
|
|
||||||
serverId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createFavoriteMutation.mutate({
|
|
||||||
query: {
|
|
||||||
id,
|
|
||||||
type: itemType,
|
|
||||||
},
|
|
||||||
serverId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[createFavoriteMutation, deleteFavoriteMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const slides = useMemo(() => {
|
|
||||||
if (!data) return [];
|
|
||||||
|
|
||||||
return data.map((el) => (
|
|
||||||
<PosterCard
|
|
||||||
controls={{
|
|
||||||
cardRows,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
itemType,
|
|
||||||
playButtonBehavior,
|
|
||||||
route,
|
|
||||||
}}
|
|
||||||
data={el}
|
|
||||||
isLoading={isLoading}
|
|
||||||
uniqueId={uniqueId}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}, [
|
|
||||||
cardRows,
|
cardRows,
|
||||||
data,
|
data,
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
isLoading,
|
|
||||||
itemType,
|
itemType,
|
||||||
playButtonBehavior,
|
|
||||||
route,
|
route,
|
||||||
|
swiperProps,
|
||||||
|
title,
|
||||||
|
isLoading,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
]);
|
}: SwiperGridCarouselProps) => {
|
||||||
|
const swiperRef = useRef<SwiperCore | any>(null);
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const [pagination, setPagination] = useState({
|
||||||
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
hasNextPage: (data?.length || 0) > Math.round(3),
|
||||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
hasPreviousPage: false,
|
||||||
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
|
|
||||||
}, [swiperProps?.slidesPerView]);
|
|
||||||
|
|
||||||
const handlePrev = useCallback(() => {
|
|
||||||
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
|
||||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
|
||||||
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
|
|
||||||
}, [swiperProps?.slidesPerView]);
|
|
||||||
|
|
||||||
const handleOnSlideChange = useCallback((e: SwiperCore) => {
|
|
||||||
const { slides, isEnd, isBeginning, params } = e;
|
|
||||||
if (isEnd || isBeginning) return;
|
|
||||||
|
|
||||||
setPagination({
|
|
||||||
hasNextPage: (params?.slidesPerView || 4) < slides.length,
|
|
||||||
hasPreviousPage: (params?.slidesPerView || 4) < slides.length,
|
|
||||||
});
|
});
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnZoomChange = useCallback((e: SwiperCore) => {
|
const createFavoriteMutation = useCreateFavorite({});
|
||||||
const { slides, isEnd, isBeginning, params } = e;
|
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||||
if (isEnd || isBeginning) return;
|
|
||||||
|
|
||||||
setPagination({
|
const handleFavorite = useCallback(
|
||||||
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
(options: {
|
||||||
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
id: string[];
|
||||||
});
|
isFavorite: boolean;
|
||||||
}, []);
|
itemType: LibraryItem;
|
||||||
|
serverId: string;
|
||||||
|
}) => {
|
||||||
|
const { id, itemType, isFavorite, serverId } = options;
|
||||||
|
if (isFavorite) {
|
||||||
|
deleteFavoriteMutation.mutate({
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
type: itemType,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createFavoriteMutation.mutate({
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
type: itemType,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[createFavoriteMutation, deleteFavoriteMutation],
|
||||||
|
);
|
||||||
|
|
||||||
const handleOnReachEnd = useCallback((e: SwiperCore) => {
|
const slides = useMemo(() => {
|
||||||
const { slides, params } = e;
|
if (!data) return [];
|
||||||
|
|
||||||
setPagination({
|
return data.map((el) => (
|
||||||
hasNextPage: false,
|
<PosterCard
|
||||||
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
controls={{
|
||||||
});
|
cardRows,
|
||||||
}, []);
|
handleFavorite,
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
itemType,
|
||||||
|
playButtonBehavior,
|
||||||
|
route,
|
||||||
|
}}
|
||||||
|
data={el}
|
||||||
|
isLoading={isLoading}
|
||||||
|
uniqueId={uniqueId}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [
|
||||||
|
cardRows,
|
||||||
|
data,
|
||||||
|
handleFavorite,
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
isLoading,
|
||||||
|
itemType,
|
||||||
|
playButtonBehavior,
|
||||||
|
route,
|
||||||
|
uniqueId,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
|
const handleNext = useCallback(() => {
|
||||||
const { slides, params } = e;
|
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
||||||
|
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
||||||
|
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
|
||||||
|
}, [swiperProps?.slidesPerView]);
|
||||||
|
|
||||||
setPagination({
|
const handlePrev = useCallback(() => {
|
||||||
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
||||||
hasPreviousPage: false,
|
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
||||||
});
|
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
|
||||||
}, []);
|
}, [swiperProps?.slidesPerView]);
|
||||||
|
|
||||||
const handleOnResize = useCallback((e: SwiperCore) => {
|
const handleOnSlideChange = useCallback((e: SwiperCore) => {
|
||||||
if (!e) return;
|
const { slides, isEnd, isBeginning, params } = e;
|
||||||
const { width } = e;
|
if (isEnd || isBeginning) return;
|
||||||
const slidesPerView = getSlidesPerView(width);
|
|
||||||
if (!e.params) return;
|
|
||||||
e.params.slidesPerView = slidesPerView;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const throttledOnResize = throttle(handleOnResize, 200);
|
setPagination({
|
||||||
|
hasNextPage: (params?.slidesPerView || 4) < slides.length,
|
||||||
|
hasPreviousPage: (params?.slidesPerView || 4) < slides.length,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
const handleOnZoomChange = useCallback((e: SwiperCore) => {
|
||||||
<CarouselContainer
|
const { slides, isEnd, isBeginning, params } = e;
|
||||||
className="grid-carousel"
|
if (isEnd || isBeginning) return;
|
||||||
spacing="md"
|
|
||||||
>
|
setPagination({
|
||||||
{title ? (
|
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
||||||
<Title
|
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
||||||
{...title}
|
});
|
||||||
handleNext={handleNext}
|
}, []);
|
||||||
handlePrev={handlePrev}
|
|
||||||
pagination={pagination}
|
const handleOnReachEnd = useCallback((e: SwiperCore) => {
|
||||||
/>
|
const { slides, params } = e;
|
||||||
) : null}
|
|
||||||
<Swiper
|
setPagination({
|
||||||
ref={swiperRef}
|
hasNextPage: false,
|
||||||
resizeObserver
|
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
||||||
modules={[Virtual]}
|
});
|
||||||
slidesPerView={4}
|
}, []);
|
||||||
spaceBetween={20}
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
|
||||||
onBeforeInit={(swiper) => {
|
const { slides, params } = e;
|
||||||
swiperRef.current = swiper;
|
|
||||||
}}
|
setPagination({
|
||||||
onBeforeResize={handleOnResize}
|
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
||||||
onReachBeginning={handleOnReachBeginning}
|
hasPreviousPage: false,
|
||||||
onReachEnd={handleOnReachEnd}
|
});
|
||||||
onResize={throttledOnResize}
|
}, []);
|
||||||
onSlideChange={handleOnSlideChange}
|
|
||||||
onZoomChange={handleOnZoomChange}
|
const handleOnResize = useCallback((e: SwiperCore) => {
|
||||||
{...swiperProps}
|
if (!e) return;
|
||||||
>
|
const { width } = e;
|
||||||
{slides.map((slideContent, index) => {
|
const slidesPerView = getSlidesPerView(width);
|
||||||
return (
|
if (!e.params) return;
|
||||||
<SwiperSlide
|
e.params.slidesPerView = slidesPerView;
|
||||||
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
}, []);
|
||||||
virtualIndex={index}
|
|
||||||
|
const throttledOnResize = throttle(handleOnResize, 200);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContainer
|
||||||
|
className="grid-carousel"
|
||||||
|
spacing="md"
|
||||||
|
>
|
||||||
|
{title ? (
|
||||||
|
<Title
|
||||||
|
{...title}
|
||||||
|
handleNext={handleNext}
|
||||||
|
handlePrev={handlePrev}
|
||||||
|
pagination={pagination}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Swiper
|
||||||
|
ref={swiperRef}
|
||||||
|
resizeObserver
|
||||||
|
modules={[Virtual]}
|
||||||
|
slidesPerView={4}
|
||||||
|
spaceBetween={20}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
onBeforeInit={(swiper) => {
|
||||||
|
swiperRef.current = swiper;
|
||||||
|
}}
|
||||||
|
onBeforeResize={handleOnResize}
|
||||||
|
onReachBeginning={handleOnReachBeginning}
|
||||||
|
onReachEnd={handleOnReachEnd}
|
||||||
|
onResize={throttledOnResize}
|
||||||
|
onSlideChange={handleOnSlideChange}
|
||||||
|
onZoomChange={handleOnZoomChange}
|
||||||
|
{...swiperProps}
|
||||||
>
|
>
|
||||||
{slideContent}
|
{slides.map((slideContent, index) => {
|
||||||
</SwiperSlide>
|
return (
|
||||||
);
|
<SwiperSlide
|
||||||
})}
|
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
||||||
</Swiper>
|
virtualIndex={index}
|
||||||
</CarouselContainer>
|
>
|
||||||
);
|
{slideContent}
|
||||||
|
</SwiperSlide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
</CarouselContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { HoverCard as MantineHoverCard, HoverCardProps } from '@mantine/core';
|
import { HoverCard as MantineHoverCard, HoverCardProps } from '@mantine/core';
|
||||||
|
|
||||||
export const HoverCard = ({ children, ...props }: HoverCardProps) => {
|
export const HoverCard = ({ children, ...props }: HoverCardProps) => {
|
||||||
return (
|
return (
|
||||||
<MantineHoverCard
|
<MantineHoverCard
|
||||||
styles={{
|
styles={{
|
||||||
dropdown: {
|
dropdown: {
|
||||||
background: 'var(--dropdown-menu-bg)',
|
background: 'var(--dropdown-menu-bg)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: 'var(--dropdown-menu-border-radius)',
|
borderRadius: 'var(--dropdown-menu-border-radius)',
|
||||||
boxShadow: '2px 2px 10px 2px rgba(0, 0, 0, 40%)',
|
boxShadow: '2px 2px 10px 2px rgba(0, 0, 0, 40%)',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MantineHoverCard>
|
</MantineHoverCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
HoverCard.Target = MantineHoverCard.Target;
|
HoverCard.Target = MantineHoverCard.Target;
|
||||||
|
@ -1,404 +1,404 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import type {
|
import type {
|
||||||
TextInputProps as MantineTextInputProps,
|
TextInputProps as MantineTextInputProps,
|
||||||
NumberInputProps as MantineNumberInputProps,
|
NumberInputProps as MantineNumberInputProps,
|
||||||
PasswordInputProps as MantinePasswordInputProps,
|
PasswordInputProps as MantinePasswordInputProps,
|
||||||
FileInputProps as MantineFileInputProps,
|
FileInputProps as MantineFileInputProps,
|
||||||
JsonInputProps as MantineJsonInputProps,
|
JsonInputProps as MantineJsonInputProps,
|
||||||
TextareaProps as MantineTextareaProps,
|
TextareaProps as MantineTextareaProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
TextInput as MantineTextInput,
|
TextInput as MantineTextInput,
|
||||||
NumberInput as MantineNumberInput,
|
NumberInput as MantineNumberInput,
|
||||||
PasswordInput as MantinePasswordInput,
|
PasswordInput as MantinePasswordInput,
|
||||||
FileInput as MantineFileInput,
|
FileInput as MantineFileInput,
|
||||||
JsonInput as MantineJsonInput,
|
JsonInput as MantineJsonInput,
|
||||||
Textarea as MantineTextarea,
|
Textarea as MantineTextarea,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface TextInputProps extends MantineTextInputProps {
|
interface TextInputProps extends MantineTextInputProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NumberInputProps extends MantineNumberInputProps {
|
interface NumberInputProps extends MantineNumberInputProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PasswordInputProps extends MantinePasswordInputProps {
|
interface PasswordInputProps extends MantinePasswordInputProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileInputProps extends MantineFileInputProps {
|
interface FileInputProps extends MantineFileInputProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JsonInputProps extends MantineJsonInputProps {
|
interface JsonInputProps extends MantineJsonInputProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextareaProps extends MantineTextareaProps {
|
interface TextareaProps extends MantineTextareaProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledTextInput = styled(MantineTextInput)<TextInputProps>`
|
const StyledTextInput = styled(MantineTextInput)<TextInputProps>`
|
||||||
& .mantine-TextInput-wrapper {
|
& .mantine-TextInput-wrapper {
|
||||||
border-color: var(--primary-color);
|
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 {
|
& .mantine-TextInput-input {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-fg);
|
||||||
}
|
background: var(--input-bg);
|
||||||
|
|
||||||
& .mantine-TextInput-required {
|
&::placeholder {
|
||||||
color: var(--secondary-color);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& .mantine-TextInput-label {
|
& .mantine-Input-icon {
|
||||||
margin-bottom: 0.5rem;
|
color: var(--input-placeholder-fg);
|
||||||
font-family: var(--label-font-family);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-TextInput-disabled {
|
& .mantine-TextInput-required {
|
||||||
opacity: 0.6;
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& .mantine-TextInput-label {
|
||||||
opacity: 0.6;
|
margin-bottom: 0.5rem;
|
||||||
}
|
font-family: var(--label-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
& .mantine-TextInput-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
& [data-disabled='true'] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: width 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledNumberInput = styled(MantineNumberInput)<NumberInputProps>`
|
const StyledNumberInput = styled(MantineNumberInput)<NumberInputProps>`
|
||||||
& .mantine-NumberInput-wrapper {
|
& .mantine-NumberInput-wrapper {
|
||||||
border-color: var(--primary-color);
|
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 {
|
& .mantine-NumberInput-input {
|
||||||
svg {
|
color: var(--input-fg);
|
||||||
color: var(--btn-default-fg);
|
background: var(--input-bg);
|
||||||
fill: var(--btn-default-fg);
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--input-placeholder-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-controlDown {
|
& .mantine-NumberInput-controlUp {
|
||||||
svg {
|
svg {
|
||||||
color: var(--btn-default-fg);
|
color: var(--btn-default-fg);
|
||||||
fill: var(--btn-default-fg);
|
fill: var(--btn-default-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Input-icon {
|
& .mantine-NumberInput-controlDown {
|
||||||
color: var(--input-placeholder-fg);
|
svg {
|
||||||
}
|
color: var(--btn-default-fg);
|
||||||
|
fill: var(--btn-default-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& .mantine-NumberInput-required {
|
& .mantine-Input-icon {
|
||||||
color: var(--secondary-color);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-NumberInput-label {
|
& .mantine-NumberInput-required {
|
||||||
margin-bottom: 0.5rem;
|
color: var(--secondary-color);
|
||||||
font-family: var(--label-font-family);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-NumberInput-disabled {
|
& .mantine-NumberInput-label {
|
||||||
opacity: 0.6;
|
margin-bottom: 0.5rem;
|
||||||
}
|
font-family: var(--label-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& .mantine-NumberInput-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
& [data-disabled='true'] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: width 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledPasswordInput = styled(MantinePasswordInput)<PasswordInputProps>`
|
const StyledPasswordInput = styled(MantinePasswordInput)<PasswordInputProps>`
|
||||||
& .mantine-PasswordInput-input {
|
& .mantine-PasswordInput-input {
|
||||||
color: var(--input-fg);
|
color: var(--input-fg);
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-PasswordInput-icon {
|
& .mantine-PasswordInput-icon {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-PasswordInput-required {
|
& .mantine-PasswordInput-required {
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-PasswordInput-label {
|
& .mantine-PasswordInput-label {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-family: var(--label-font-family);
|
font-family: var(--label-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-PasswordInput-disabled {
|
& .mantine-PasswordInput-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
transition: width 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledFileInput = styled(MantineFileInput)<FileInputProps>`
|
const StyledFileInput = styled(MantineFileInput)<FileInputProps>`
|
||||||
& .mantine-FileInput-input {
|
& .mantine-FileInput-input {
|
||||||
color: var(--input-fg);
|
color: var(--input-fg);
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-FileInput-icon {
|
& .mantine-FileInput-icon {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-FileInput-required {
|
& .mantine-FileInput-required {
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-FileInput-label {
|
& .mantine-FileInput-label {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-family: var(--label-font-family);
|
font-family: var(--label-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-FileInput-disabled {
|
& .mantine-FileInput-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
transition: width 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledJsonInput = styled(MantineJsonInput)<JsonInputProps>`
|
const StyledJsonInput = styled(MantineJsonInput)<JsonInputProps>`
|
||||||
& .mantine-JsonInput-input {
|
& .mantine-JsonInput-input {
|
||||||
color: var(--input-fg);
|
color: var(--input-fg);
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-JsonInput-icon {
|
& .mantine-JsonInput-icon {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-JsonInput-required {
|
& .mantine-JsonInput-required {
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-JsonInput-label {
|
& .mantine-JsonInput-label {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-family: var(--label-font-family);
|
font-family: var(--label-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-JsonInput-disabled {
|
& .mantine-JsonInput-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
transition: width 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTextarea = styled(MantineTextarea)<TextareaProps>`
|
const StyledTextarea = styled(MantineTextarea)<TextareaProps>`
|
||||||
& .mantine-Textarea-input {
|
& .mantine-Textarea-input {
|
||||||
color: var(--input-fg);
|
color: var(--input-fg);
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Textarea-icon {
|
& .mantine-Textarea-icon {
|
||||||
color: var(--input-placeholder-fg);
|
color: var(--input-placeholder-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Textarea-required {
|
& .mantine-Textarea-required {
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Textarea-label {
|
& .mantine-Textarea-label {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-family: var(--label-font-family);
|
font-family: var(--label-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Textarea-disabled {
|
& .mantine-Textarea-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: width 0.3s ease-in-out;
|
transition: width 0.3s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||||
({ children, width, maxWidth, ...props }: TextInputProps, ref) => {
|
({ children, width, maxWidth, ...props }: TextInputProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledTextInput
|
<StyledTextInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
{...props}
|
{...props}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledTextInput>
|
</StyledTextInput>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
||||||
({ children, width, maxWidth, ...props }: NumberInputProps, ref) => {
|
({ children, width, maxWidth, ...props }: NumberInputProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledNumberInput
|
<StyledNumberInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hideControls
|
hideControls
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
{...props}
|
{...props}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledNumberInput>
|
</StyledNumberInput>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||||
({ children, width, maxWidth, ...props }: PasswordInputProps, ref) => {
|
({ children, width, maxWidth, ...props }: PasswordInputProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledPasswordInput
|
<StyledPasswordInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledPasswordInput>
|
</StyledPasswordInput>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
|
export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
|
||||||
({ children, width, maxWidth, ...props }: FileInputProps, ref) => {
|
({ children, width, maxWidth, ...props }: FileInputProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledFileInput
|
<StyledFileInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
styles={{
|
styles={{
|
||||||
placeholder: {
|
placeholder: {
|
||||||
color: 'var(--input-placeholder-fg)',
|
color: 'var(--input-placeholder-fg)',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledFileInput>
|
</StyledFileInput>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
|
export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
|
||||||
({ children, width, maxWidth, ...props }: JsonInputProps, ref) => {
|
({ children, width, maxWidth, ...props }: JsonInputProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledJsonInput
|
<StyledJsonInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledJsonInput>
|
</StyledJsonInput>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ children, width, maxWidth, ...props }: TextareaProps, ref) => {
|
({ children, width, maxWidth, ...props }: TextareaProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledTextarea
|
<StyledTextarea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledTextarea>
|
</StyledTextarea>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
TextInput.defaultProps = {
|
TextInput.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
NumberInput.defaultProps = {
|
NumberInput.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
PasswordInput.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
FileInput.defaultProps = {
|
FileInput.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
JsonInput.defaultProps = {
|
JsonInput.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
Textarea.defaultProps = {
|
Textarea.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
@ -1,99 +1,99 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
ModalProps as MantineModalProps,
|
ModalProps as MantineModalProps,
|
||||||
Stack,
|
Stack,
|
||||||
Modal as MantineModal,
|
Modal as MantineModal,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
||||||
import { Button } from '/@/renderer/components/button';
|
import { Button } from '/@/renderer/components/button';
|
||||||
|
|
||||||
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
|
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
handlers: {
|
handlers: {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
open: () => void;
|
open: () => void;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = ({ children, handlers, ...rest }: ModalProps) => {
|
export const Modal = ({ children, handlers, ...rest }: ModalProps) => {
|
||||||
return (
|
return (
|
||||||
<MantineModal
|
<MantineModal
|
||||||
{...rest}
|
{...rest}
|
||||||
onClose={handlers.close}
|
onClose={handlers.close}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MantineModal>
|
</MantineModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextModalVars = {
|
export type ContextModalVars = {
|
||||||
context: ContextModalProps['context'];
|
context: ContextModalProps['context'];
|
||||||
id: ContextModalProps['id'];
|
id: ContextModalProps['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BaseContextModal = ({
|
export const BaseContextModal = ({
|
||||||
context,
|
context,
|
||||||
id,
|
id,
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<{
|
}: ContextModalProps<{
|
||||||
modalBody: (vars: ContextModalVars) => React.ReactNode;
|
modalBody: (vars: ContextModalVars) => React.ReactNode;
|
||||||
}>) => <>{innerProps.modalBody({ context, id })}</>;
|
}>) => <>{innerProps.modalBody({ context, id })}</>;
|
||||||
|
|
||||||
Modal.defaultProps = {
|
Modal.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
labels?: {
|
labels?: {
|
||||||
cancel?: string;
|
cancel?: string;
|
||||||
confirm?: string;
|
confirm?: string;
|
||||||
};
|
};
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmModal = ({
|
export const ConfirmModal = ({
|
||||||
loading,
|
loading,
|
||||||
disabled,
|
disabled,
|
||||||
labels,
|
labels,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
children,
|
children,
|
||||||
}: ConfirmModalProps) => {
|
}: ConfirmModalProps) => {
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
onCancel();
|
onCancel();
|
||||||
} else {
|
} else {
|
||||||
closeAllModals();
|
closeAllModals();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Flex>{children}</Flex>
|
<Flex>{children}</Flex>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button
|
<Button
|
||||||
data-focus
|
data-focus
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
>
|
>
|
||||||
{labels?.cancel ? labels.cancel : 'Cancel'}
|
{labels?.cancel ? labels.cancel : 'Cancel'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
>
|
>
|
||||||
{labels?.confirm ? labels.confirm : 'Confirm'}
|
{labels?.confirm ? labels.confirm : 'Confirm'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,30 +2,30 @@ import { ReactNode } from 'react';
|
|||||||
import { Flex, Group } from '@mantine/core';
|
import { Flex, Group } from '@mantine/core';
|
||||||
|
|
||||||
export const Option = ({ children }: any) => {
|
export const Option = ({ children }: any) => {
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
grow
|
grow
|
||||||
p="0.5rem"
|
p="0.5rem"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface LabelProps {
|
interface LabelProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Label = ({ children }: LabelProps) => {
|
const Label = ({ children }: LabelProps) => {
|
||||||
return <Flex align="flex-start">{children}</Flex>;
|
return <Flex align="flex-start">{children}</Flex>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ControlProps {
|
interface ControlProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Control = ({ children }: ControlProps) => {
|
const Control = ({ children }: ControlProps) => {
|
||||||
return <Flex justify="flex-end">{children}</Flex>;
|
return <Flex justify="flex-end">{children}</Flex>;
|
||||||
};
|
};
|
||||||
|
|
||||||
Option.Label = Label;
|
Option.Label = Label;
|
||||||
|
@ -7,127 +7,127 @@ import { useWindowSettings } from '/@/renderer/store/settings.store';
|
|||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
const Container = styled(motion(Flex))<{
|
const Container = styled(motion(Flex))<{
|
||||||
height?: string;
|
height?: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
}>`
|
}>`
|
||||||
position: ${(props) => props.position || 'relative'};
|
position: ${(props) => props.position || 'relative'};
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: ${(props) => props.height || '65px'};
|
height: ${(props) => props.height || '65px'};
|
||||||
background: var(--titlebar-bg);
|
background: var(--titlebar-bg);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Header = styled(motion.div)<{
|
const Header = styled(motion.div)<{
|
||||||
$isDraggable?: boolean;
|
$isDraggable?: boolean;
|
||||||
$isHidden?: boolean;
|
$isHidden?: boolean;
|
||||||
$padRight?: boolean;
|
$padRight?: boolean;
|
||||||
}>`
|
}>`
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-right: ${(props) => (props.$padRight ? '140px' : '1rem')};
|
margin-right: ${(props) => (props.$padRight ? '140px' : '1rem')};
|
||||||
user-select: ${(props) => (props.$isHidden ? 'none' : 'auto')};
|
user-select: ${(props) => (props.$isHidden ? 'none' : 'auto')};
|
||||||
pointer-events: ${(props) => (props.$isHidden ? 'none' : 'auto')};
|
pointer-events: ${(props) => (props.$isHidden ? 'none' : 'auto')};
|
||||||
-webkit-app-region: ${(props) => props.$isDraggable && 'drag'};
|
-webkit-app-region: ${(props) => props.$isDraggable && 'drag'};
|
||||||
|
|
||||||
button {
|
button {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BackgroundImage = styled.div<{ background: string }>`
|
const BackgroundImage = styled.div<{ background: string }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: ${(props) => props.background || 'var(--titlebar-bg)'};
|
background: ${(props) => props.background || 'var(--titlebar-bg)'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
|
const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: ${(props) =>
|
background: ${(props) =>
|
||||||
props.theme === 'light'
|
props.theme === 'light'
|
||||||
? 'linear-gradient(rgba(255, 255, 255, 25%), rgba(255, 255, 255, 25%))'
|
? 'linear-gradient(rgba(255, 255, 255, 25%), rgba(255, 255, 255, 25%))'
|
||||||
: 'linear-gradient(rgba(0, 0, 0, 50%), rgba(0, 0, 0, 50%))'};
|
: 'linear-gradient(rgba(0, 0, 0, 50%), rgba(0, 0, 0, 50%))'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface PageHeaderProps
|
export interface PageHeaderProps
|
||||||
extends Omit<FlexProps, 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'> {
|
extends Omit<FlexProps, 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'> {
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
height?: string;
|
height?: string;
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
position?: string;
|
position?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TitleWrapper = styled(motion.div)`
|
const TitleWrapper = styled(motion.div)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
animate: { opacity: 1 },
|
animate: { opacity: 1 },
|
||||||
exit: { opacity: 0 },
|
exit: { opacity: 0 },
|
||||||
initial: { opacity: 0 },
|
initial: { opacity: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader = ({
|
export const PageHeader = ({
|
||||||
position,
|
position,
|
||||||
height,
|
height,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
isHidden,
|
isHidden,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: PageHeaderProps) => {
|
}: PageHeaderProps) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const padRight = useShouldPadTitlebar();
|
const padRight = useShouldPadTitlebar();
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
ref={ref}
|
ref={ref}
|
||||||
height={height}
|
height={height}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Header
|
<Header
|
||||||
$isDraggable={windowBarStyle === Platform.WEB}
|
$isDraggable={windowBarStyle === Platform.WEB}
|
||||||
$isHidden={isHidden}
|
$isHidden={isHidden}
|
||||||
$padRight={padRight}
|
$padRight={padRight}
|
||||||
>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{!isHidden && (
|
|
||||||
<TitleWrapper
|
|
||||||
animate="animate"
|
|
||||||
exit="exit"
|
|
||||||
initial="initial"
|
|
||||||
variants={variants}
|
|
||||||
>
|
>
|
||||||
{children}
|
<AnimatePresence initial={false}>
|
||||||
</TitleWrapper>
|
{!isHidden && (
|
||||||
)}
|
<TitleWrapper
|
||||||
</AnimatePresence>
|
animate="animate"
|
||||||
</Header>
|
exit="exit"
|
||||||
{backgroundColor && (
|
initial="initial"
|
||||||
<>
|
variants={variants}
|
||||||
<BackgroundImage background={backgroundColor || 'var(--titlebar-bg)'} />
|
>
|
||||||
<BackgroundImageOverlay theme={theme} />
|
{children}
|
||||||
</>
|
</TitleWrapper>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</AnimatePresence>
|
||||||
);
|
</Header>
|
||||||
|
{backgroundColor && (
|
||||||
|
<>
|
||||||
|
<BackgroundImage background={backgroundColor || 'var(--titlebar-bg)'} />
|
||||||
|
<BackgroundImageOverlay theme={theme} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,52 +1,52 @@
|
|||||||
import {
|
import {
|
||||||
Pagination as MantinePagination,
|
Pagination as MantinePagination,
|
||||||
PaginationProps as MantinePaginationProps,
|
PaginationProps as MantinePaginationProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledPagination = styled(MantinePagination)<PaginationProps>`
|
const StyledPagination = styled(MantinePagination)<PaginationProps>`
|
||||||
& .mantine-Pagination-item {
|
& .mantine-Pagination-item {
|
||||||
color: var(--btn-default-fg);
|
color: var(--btn-default-fg);
|
||||||
background-color: var(--btn-default-bg);
|
background-color: var(--btn-default-bg);
|
||||||
border: none;
|
border: none;
|
||||||
transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||||
|
|
||||||
&[data-active] {
|
&[data-active] {
|
||||||
color: var(--btn-primary-fg);
|
color: var(--btn-primary-fg);
|
||||||
background-color: var(--btn-primary-bg);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[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 {
|
interface PaginationProps extends MantinePaginationProps {
|
||||||
$hideDividers?: boolean;
|
$hideDividers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Pagination = ({ $hideDividers, ...props }: PaginationProps) => {
|
export const Pagination = ({ $hideDividers, ...props }: PaginationProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledPagination
|
<StyledPagination
|
||||||
$hideDividers={$hideDividers}
|
$hideDividers={$hideDividers}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,13 +3,13 @@ import { Paper as MantinePaper } from '@mantine/core';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export interface PaperProps extends MantinePaperProps {
|
export interface PaperProps extends MantinePaperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledPaper = styled(MantinePaper)<PaperProps>`
|
const StyledPaper = styled(MantinePaper)<PaperProps>`
|
||||||
background: var(--paper-bg);
|
background: var(--paper-bg);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Paper = ({ children, ...props }: PaperProps) => {
|
export const Paper = ({ children, ...props }: PaperProps) => {
|
||||||
return <StyledPaper {...props}>{children}</StyledPaper>;
|
return <StyledPaper {...props}>{children}</StyledPaper>;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
PopoverProps as MantinePopoverProps,
|
PopoverProps as MantinePopoverProps,
|
||||||
PopoverDropdownProps as MantinePopoverDropdownProps,
|
PopoverDropdownProps as MantinePopoverDropdownProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Popover as MantinePopover } from '@mantine/core';
|
import { Popover as MantinePopover } from '@mantine/core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -11,28 +11,28 @@ type PopoverDropdownProps = MantinePopoverDropdownProps;
|
|||||||
const StyledPopover = styled(MantinePopover)``;
|
const StyledPopover = styled(MantinePopover)``;
|
||||||
|
|
||||||
const StyledDropdown = styled(MantinePopover.Dropdown)<PopoverDropdownProps>`
|
const StyledDropdown = styled(MantinePopover.Dropdown)<PopoverDropdownProps>`
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-family: var(--content-font-family);
|
font-family: var(--content-font-family);
|
||||||
background-color: var(--dropdown-menu-bg);
|
background-color: var(--dropdown-menu-bg);
|
||||||
border: var(--dropdown-menu-border);
|
border: var(--dropdown-menu-border);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Popover = ({ children, ...props }: PopoverProps) => {
|
export const Popover = ({ children, ...props }: PopoverProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledPopover
|
<StyledPopover
|
||||||
withinPortal
|
withinPortal
|
||||||
styles={{
|
styles={{
|
||||||
dropdown: {
|
dropdown: {
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
transitionProps={{ transition: 'fade' }}
|
transitionProps={{ transition: 'fade' }}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledPopover>
|
</StyledPopover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Popover.Target = MantinePopover.Target;
|
Popover.Target = MantinePopover.Target;
|
||||||
|
@ -8,211 +8,211 @@ import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-b
|
|||||||
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
|
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
|
||||||
|
|
||||||
const FILTER_GROUP_OPTIONS_DATA = [
|
const FILTER_GROUP_OPTIONS_DATA = [
|
||||||
{
|
{
|
||||||
label: 'Match all',
|
label: 'Match all',
|
||||||
value: 'all',
|
value: 'all',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Match any',
|
label: 'Match any',
|
||||||
value: 'any',
|
value: 'any',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type AddArgs = {
|
type AddArgs = {
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeleteArgs = {
|
type DeleteArgs = {
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
};
|
};
|
||||||
interface QueryBuilderProps {
|
interface QueryBuilderProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
filters: { label: string; type: string; value: string }[];
|
filters: { label: string; type: string; value: string }[];
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
onAddRule: (args: AddArgs) => void;
|
onAddRule: (args: AddArgs) => void;
|
||||||
onAddRuleGroup: (args: AddArgs) => void;
|
onAddRuleGroup: (args: AddArgs) => void;
|
||||||
onChangeField: (args: any) => void;
|
onChangeField: (args: any) => void;
|
||||||
onChangeOperator: (args: any) => void;
|
onChangeOperator: (args: any) => void;
|
||||||
onChangeType: (args: any) => void;
|
onChangeType: (args: any) => void;
|
||||||
onChangeValue: (args: any) => void;
|
onChangeValue: (args: any) => void;
|
||||||
onClearFilters: () => void;
|
onClearFilters: () => void;
|
||||||
onDeleteRule: (args: DeleteArgs) => void;
|
onDeleteRule: (args: DeleteArgs) => void;
|
||||||
onDeleteRuleGroup: (args: DeleteArgs) => void;
|
onDeleteRuleGroup: (args: DeleteArgs) => void;
|
||||||
onResetFilters: () => void;
|
onResetFilters: () => void;
|
||||||
operators: {
|
operators: {
|
||||||
boolean: { label: string; value: string }[];
|
boolean: { label: string; value: string }[];
|
||||||
date: { label: string; value: string }[];
|
date: { label: string; value: string }[];
|
||||||
number: { label: string; value: string }[];
|
number: { label: string; value: string }[];
|
||||||
string: { label: string; value: string }[];
|
string: { label: string; value: string }[];
|
||||||
};
|
};
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryBuilder = ({
|
export const QueryBuilder = ({
|
||||||
data,
|
data,
|
||||||
level,
|
level,
|
||||||
onAddRule,
|
onAddRule,
|
||||||
onDeleteRuleGroup,
|
onDeleteRuleGroup,
|
||||||
onDeleteRule,
|
onDeleteRule,
|
||||||
onAddRuleGroup,
|
onAddRuleGroup,
|
||||||
onChangeType,
|
onChangeType,
|
||||||
onChangeField,
|
onChangeField,
|
||||||
operators,
|
operators,
|
||||||
onChangeOperator,
|
onChangeOperator,
|
||||||
onChangeValue,
|
onChangeValue,
|
||||||
onClearFilters,
|
onClearFilters,
|
||||||
onResetFilters,
|
onResetFilters,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
filters,
|
filters,
|
||||||
}: QueryBuilderProps) => {
|
}: QueryBuilderProps) => {
|
||||||
const handleAddRule = () => {
|
const handleAddRule = () => {
|
||||||
onAddRule({ groupIndex, level });
|
onAddRule({ groupIndex, level });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRuleGroup = () => {
|
const handleAddRuleGroup = () => {
|
||||||
onAddRuleGroup({ groupIndex, level });
|
onAddRuleGroup({ groupIndex, level });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRuleGroup = () => {
|
const handleDeleteRuleGroup = () => {
|
||||||
onDeleteRuleGroup({ groupIndex, level, uniqueId });
|
onDeleteRuleGroup({ groupIndex, level, uniqueId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeType = (value: string | null) => {
|
const handleChangeType = (value: string | null) => {
|
||||||
onChangeType({ groupIndex, level, value });
|
onChangeType({ groupIndex, level, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
ml={`${level * 10}px`}
|
ml={`${level * 10}px`}
|
||||||
spacing="sm"
|
spacing="sm"
|
||||||
>
|
|
||||||
<Group spacing="sm">
|
|
||||||
<Select
|
|
||||||
data={FILTER_GROUP_OPTIONS_DATA}
|
|
||||||
maxWidth={175}
|
|
||||||
size="sm"
|
|
||||||
value={data.type}
|
|
||||||
width="20%"
|
|
||||||
onChange={handleChangeType}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
px={5}
|
|
||||||
size="sm"
|
|
||||||
tooltip={{ label: 'Add rule' }}
|
|
||||||
variant="default"
|
|
||||||
onClick={handleAddRule}
|
|
||||||
>
|
>
|
||||||
<RiAddLine size={20} />
|
<Group spacing="sm">
|
||||||
</Button>
|
<Select
|
||||||
<DropdownMenu position="bottom-start">
|
data={FILTER_GROUP_OPTIONS_DATA}
|
||||||
<DropdownMenu.Target>
|
maxWidth={175}
|
||||||
<Button
|
size="sm"
|
||||||
p={0}
|
value={data.type}
|
||||||
size="sm"
|
width="20%"
|
||||||
variant="subtle"
|
onChange={handleChangeType}
|
||||||
>
|
/>
|
||||||
<RiMore2Line size={20} />
|
<Button
|
||||||
</Button>
|
px={5}
|
||||||
</DropdownMenu.Target>
|
size="sm"
|
||||||
<DropdownMenu.Dropdown>
|
tooltip={{ label: 'Add rule' }}
|
||||||
<DropdownMenu.Item
|
variant="default"
|
||||||
icon={<RiAddFill />}
|
onClick={handleAddRule}
|
||||||
onClick={handleAddRuleGroup}
|
>
|
||||||
>
|
<RiAddLine size={20} />
|
||||||
Add rule group
|
</Button>
|
||||||
</DropdownMenu.Item>
|
<DropdownMenu position="bottom-start">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
p={0}
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<RiMore2Line size={20} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
icon={<RiAddFill />}
|
||||||
|
onClick={handleAddRuleGroup}
|
||||||
|
>
|
||||||
|
Add rule group
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiDeleteBinFill />}
|
icon={<RiDeleteBinFill />}
|
||||||
onClick={handleDeleteRuleGroup}
|
onClick={handleDeleteRuleGroup}
|
||||||
>
|
>
|
||||||
Remove rule group
|
Remove rule group
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
)}
|
||||||
|
{level === 0 && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$danger
|
||||||
|
icon={<RiRestartLine color="var(--danger-color)" />}
|
||||||
|
onClick={onResetFilters}
|
||||||
|
>
|
||||||
|
Reset to default
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$danger
|
||||||
|
icon={<RiDeleteBinFill color="var(--danger-color)" />}
|
||||||
|
onClick={onClearFilters}
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Group>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{data?.rules?.map((rule: QueryBuilderRule) => (
|
||||||
|
<motion.div
|
||||||
|
key={rule.uniqueId}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -25 }}
|
||||||
|
initial={{ opacity: 0, x: -25 }}
|
||||||
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
<QueryBuilderOption
|
||||||
|
data={rule}
|
||||||
|
filters={filters}
|
||||||
|
groupIndex={groupIndex || []}
|
||||||
|
level={level}
|
||||||
|
noRemove={data?.rules?.length === 1}
|
||||||
|
operators={operators}
|
||||||
|
onChangeField={onChangeField}
|
||||||
|
onChangeOperator={onChangeOperator}
|
||||||
|
onChangeValue={onChangeValue}
|
||||||
|
onDeleteRule={onDeleteRule}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
{data?.group && (
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{data.group?.map((group: QueryBuilderGroup, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={group.uniqueId}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -25 }}
|
||||||
|
initial={{ opacity: 0, x: -25 }}
|
||||||
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
<QueryBuilder
|
||||||
|
data={group}
|
||||||
|
filters={filters}
|
||||||
|
groupIndex={[...(groupIndex || []), index]}
|
||||||
|
level={level + 1}
|
||||||
|
operators={operators}
|
||||||
|
uniqueId={group.uniqueId}
|
||||||
|
onAddRule={onAddRule}
|
||||||
|
onAddRuleGroup={onAddRuleGroup}
|
||||||
|
onChangeField={onChangeField}
|
||||||
|
onChangeOperator={onChangeOperator}
|
||||||
|
onChangeType={onChangeType}
|
||||||
|
onChangeValue={onChangeValue}
|
||||||
|
onClearFilters={onClearFilters}
|
||||||
|
onDeleteRule={onDeleteRule}
|
||||||
|
onDeleteRuleGroup={onDeleteRuleGroup}
|
||||||
|
onResetFilters={onResetFilters}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
)}
|
)}
|
||||||
{level === 0 && (
|
</Stack>
|
||||||
<>
|
);
|
||||||
<DropdownMenu.Divider />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
$danger
|
|
||||||
icon={<RiRestartLine color="var(--danger-color)" />}
|
|
||||||
onClick={onResetFilters}
|
|
||||||
>
|
|
||||||
Reset to default
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
$danger
|
|
||||||
icon={<RiDeleteBinFill color="var(--danger-color)" />}
|
|
||||||
onClick={onClearFilters}
|
|
||||||
>
|
|
||||||
Clear filters
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Group>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{data?.rules?.map((rule: QueryBuilderRule) => (
|
|
||||||
<motion.div
|
|
||||||
key={rule.uniqueId}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -25 }}
|
|
||||||
initial={{ opacity: 0, x: -25 }}
|
|
||||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
||||||
>
|
|
||||||
<QueryBuilderOption
|
|
||||||
data={rule}
|
|
||||||
filters={filters}
|
|
||||||
groupIndex={groupIndex || []}
|
|
||||||
level={level}
|
|
||||||
noRemove={data?.rules?.length === 1}
|
|
||||||
operators={operators}
|
|
||||||
onChangeField={onChangeField}
|
|
||||||
onChangeOperator={onChangeOperator}
|
|
||||||
onChangeValue={onChangeValue}
|
|
||||||
onDeleteRule={onDeleteRule}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
{data?.group && (
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{data.group?.map((group: QueryBuilderGroup, index: number) => (
|
|
||||||
<motion.div
|
|
||||||
key={group.uniqueId}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -25 }}
|
|
||||||
initial={{ opacity: 0, x: -25 }}
|
|
||||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
||||||
>
|
|
||||||
<QueryBuilder
|
|
||||||
data={group}
|
|
||||||
filters={filters}
|
|
||||||
groupIndex={[...(groupIndex || []), index]}
|
|
||||||
level={level + 1}
|
|
||||||
operators={operators}
|
|
||||||
uniqueId={group.uniqueId}
|
|
||||||
onAddRule={onAddRule}
|
|
||||||
onAddRuleGroup={onAddRuleGroup}
|
|
||||||
onChangeField={onChangeField}
|
|
||||||
onChangeOperator={onChangeOperator}
|
|
||||||
onChangeType={onChangeType}
|
|
||||||
onChangeValue={onChangeValue}
|
|
||||||
onClearFilters={onClearFilters}
|
|
||||||
onDeleteRule={onDeleteRule}
|
|
||||||
onDeleteRuleGroup={onDeleteRuleGroup}
|
|
||||||
onResetFilters={onResetFilters}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -7,233 +7,233 @@ import { Select } from '/@/renderer/components/select';
|
|||||||
import { QueryBuilderRule } from '/@/renderer/types';
|
import { QueryBuilderRule } from '/@/renderer/types';
|
||||||
|
|
||||||
type DeleteArgs = {
|
type DeleteArgs = {
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface QueryOptionProps {
|
interface QueryOptionProps {
|
||||||
data: QueryBuilderRule;
|
data: QueryBuilderRule;
|
||||||
filters: { label: string; type: string; value: string }[];
|
filters: { label: string; type: string; value: string }[];
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
noRemove: boolean;
|
noRemove: boolean;
|
||||||
onChangeField: (args: any) => void;
|
onChangeField: (args: any) => void;
|
||||||
onChangeOperator: (args: any) => void;
|
onChangeOperator: (args: any) => void;
|
||||||
onChangeValue: (args: any) => void;
|
onChangeValue: (args: any) => void;
|
||||||
onDeleteRule: (args: DeleteArgs) => void;
|
onDeleteRule: (args: DeleteArgs) => void;
|
||||||
operators: {
|
operators: {
|
||||||
boolean: { label: string; value: string }[];
|
boolean: { label: string; value: string }[];
|
||||||
date: { label: string; value: string }[];
|
date: { label: string; value: string }[];
|
||||||
number: { label: string; value: string }[];
|
number: { label: string; value: string }[];
|
||||||
string: { label: string; value: string }[];
|
string: { label: string; value: string }[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryValueInput = ({ onChange, type, ...props }: any) => {
|
const QueryValueInput = ({ onChange, type, ...props }: any) => {
|
||||||
const [numberRange, setNumberRange] = useState([0, 0]);
|
const [numberRange, setNumberRange] = useState([0, 0]);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'number':
|
case 'number':
|
||||||
return (
|
return (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...props}
|
{...props}
|
||||||
defaultValue={props.defaultValue && Number(props.defaultValue)}
|
defaultValue={props.defaultValue && Number(props.defaultValue)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'date':
|
case 'date':
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'dateRange':
|
case 'dateRange':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
{...props}
|
{...props}
|
||||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
|
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
|
||||||
maxWidth={81}
|
maxWidth={81}
|
||||||
width="10%"
|
width="10%"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newRange = [e || 0, numberRange[1]];
|
const newRange = [e || 0, numberRange[1]];
|
||||||
setNumberRange(newRange);
|
setNumberRange(newRange);
|
||||||
onChange(newRange);
|
onChange(newRange);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
{...props}
|
{...props}
|
||||||
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
|
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
|
||||||
maxWidth={81}
|
maxWidth={81}
|
||||||
width="10%"
|
width="10%"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newRange = [numberRange[0], e || 0];
|
const newRange = [numberRange[0], e || 0];
|
||||||
setNumberRange(newRange);
|
setNumberRange(newRange);
|
||||||
onChange(newRange);
|
onChange(newRange);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
data={[
|
data={[
|
||||||
{ label: 'true', value: 'true' },
|
{ label: 'true', value: 'true' },
|
||||||
{ label: 'false', value: 'false' },
|
{ label: 'false', value: 'false' },
|
||||||
]}
|
]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryBuilderOption = ({
|
export const QueryBuilderOption = ({
|
||||||
data,
|
data,
|
||||||
filters,
|
filters,
|
||||||
level,
|
level,
|
||||||
onDeleteRule,
|
onDeleteRule,
|
||||||
operators,
|
operators,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
noRemove,
|
noRemove,
|
||||||
onChangeField,
|
onChangeField,
|
||||||
onChangeOperator,
|
onChangeOperator,
|
||||||
onChangeValue,
|
onChangeValue,
|
||||||
}: QueryOptionProps) => {
|
}: QueryOptionProps) => {
|
||||||
const { field, operator, uniqueId, value } = data;
|
const { field, operator, uniqueId, value } = data;
|
||||||
|
|
||||||
const handleDeleteRule = () => {
|
const handleDeleteRule = () => {
|
||||||
onDeleteRule({ groupIndex, level, uniqueId });
|
onDeleteRule({ groupIndex, level, uniqueId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeField = (e: any) => {
|
const handleChangeField = (e: any) => {
|
||||||
onChangeField({ groupIndex, level, uniqueId, value: e });
|
onChangeField({ groupIndex, level, uniqueId, value: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeOperator = (e: any) => {
|
const handleChangeOperator = (e: any) => {
|
||||||
onChangeOperator({ groupIndex, level, uniqueId, value: e });
|
onChangeOperator({ groupIndex, level, uniqueId, value: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeValue = (e: any) => {
|
const handleChangeValue = (e: any) => {
|
||||||
const isDirectValue =
|
const isDirectValue =
|
||||||
typeof e === 'string' ||
|
typeof e === 'string' ||
|
||||||
typeof e === 'number' ||
|
typeof e === 'number' ||
|
||||||
typeof e === 'undefined' ||
|
typeof e === 'undefined' ||
|
||||||
typeof e === null;
|
typeof e === null;
|
||||||
|
|
||||||
if (isDirectValue) {
|
if (isDirectValue) {
|
||||||
return onChangeValue({
|
return onChangeValue({
|
||||||
groupIndex,
|
groupIndex,
|
||||||
level,
|
level,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
value: e,
|
value: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// const isDate = e instanceof Date;
|
// const isDate = e instanceof Date;
|
||||||
|
|
||||||
// if (isDate) {
|
// if (isDate) {
|
||||||
// return onChangeValue({
|
// return onChangeValue({
|
||||||
// groupIndex,
|
// groupIndex,
|
||||||
// level,
|
// level,
|
||||||
// uniqueId,
|
// uniqueId,
|
||||||
// value: dayjs(e).format('YYYY-MM-DD'),
|
// value: dayjs(e).format('YYYY-MM-DD'),
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const isArray = Array.isArray(e);
|
const isArray = Array.isArray(e);
|
||||||
|
|
||||||
if (isArray) {
|
if (isArray) {
|
||||||
return onChangeValue({
|
return onChangeValue({
|
||||||
groupIndex,
|
groupIndex,
|
||||||
level,
|
level,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
value: e,
|
value: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return onChangeValue({
|
return onChangeValue({
|
||||||
groupIndex,
|
groupIndex,
|
||||||
level,
|
level,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
value: e.currentTarget.value,
|
value: e.currentTarget.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldType = filters.find((f) => f.value === field)?.type;
|
const fieldType = filters.find((f) => f.value === field)?.type;
|
||||||
const operatorsByFieldType = operators[fieldType as keyof typeof operators];
|
const operatorsByFieldType = operators[fieldType as keyof typeof operators];
|
||||||
const ml = (level + 1) * 10;
|
const ml = (level + 1) * 10;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
ml={ml}
|
ml={ml}
|
||||||
spacing="sm"
|
spacing="sm"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={filters}
|
data={filters}
|
||||||
maxWidth={170}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={field}
|
value={field}
|
||||||
width="25%"
|
width="25%"
|
||||||
onChange={handleChangeField}
|
onChange={handleChangeField}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={operatorsByFieldType || []}
|
data={operatorsByFieldType || []}
|
||||||
disabled={!field}
|
disabled={!field}
|
||||||
maxWidth={170}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={operator}
|
value={operator}
|
||||||
width="25%"
|
width="25%"
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
/>
|
/>
|
||||||
{field ? (
|
{field ? (
|
||||||
<QueryValueInput
|
<QueryValueInput
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
maxWidth={170}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
type={operator === 'inTheRange' ? 'dateRange' : fieldType}
|
type={operator === 'inTheRange' ? 'dateRange' : fieldType}
|
||||||
width="25%"
|
width="25%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled
|
disabled
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
maxWidth={170}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
width="25%"
|
width="25%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={noRemove}
|
disabled={noRemove}
|
||||||
px={5}
|
px={5}
|
||||||
size="sm"
|
size="sm"
|
||||||
tooltip={{ label: 'Remove rule' }}
|
tooltip={{ label: 'Remove rule' }}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleDeleteRule}
|
onClick={handleDeleteRule}
|
||||||
>
|
>
|
||||||
<RiSubtractLine size={20} />
|
<RiSubtractLine size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,29 +5,29 @@ import styled from 'styled-components';
|
|||||||
import { Tooltip } from '/@/renderer/components/tooltip';
|
import { Tooltip } from '/@/renderer/components/tooltip';
|
||||||
|
|
||||||
interface RatingProps extends Omit<MantineRatingProps, 'onClick'> {
|
interface RatingProps extends Omit<MantineRatingProps, 'onClick'> {
|
||||||
onClick: (e: MouseEvent<HTMLDivElement>, value: number | undefined) => void;
|
onClick: (e: MouseEvent<HTMLDivElement>, value: number | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledRating = styled(MantineRating)`
|
const StyledRating = styled(MantineRating)`
|
||||||
& .mantine-Rating-symbolBody {
|
& .mantine-Rating-symbolBody {
|
||||||
svg {
|
svg {
|
||||||
stroke: var(--main-fg-secondary);
|
stroke: var(--main-fg-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Rating = ({ onClick, ...props }: RatingProps) => {
|
export const Rating = ({ onClick, ...props }: RatingProps) => {
|
||||||
// const debouncedOnClick = debounce(onClick, 100);
|
// const debouncedOnClick = debounce(onClick, 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label="Double click to clear"
|
label="Double click to clear"
|
||||||
openDelay={1000}
|
openDelay={1000}
|
||||||
>
|
>
|
||||||
<StyledRating
|
<StyledRating
|
||||||
{...props}
|
{...props}
|
||||||
onDoubleClick={(e) => onClick(e, props.value)}
|
onDoubleClick={(e) => onClick(e, props.value)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,169 +9,169 @@ import { useWindowSettings } from '/@/renderer/store/settings.store';
|
|||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
interface ScrollAreaProps extends MantineScrollAreaProps {
|
interface ScrollAreaProps extends MantineScrollAreaProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledScrollArea = styled(MantineScrollArea)`
|
const StyledScrollArea = styled(MantineScrollArea)`
|
||||||
& .mantine-ScrollArea-thumb {
|
& .mantine-ScrollArea-thumb {
|
||||||
background: var(--scrollbar-thumb-bg);
|
background: var(--scrollbar-thumb-bg);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-ScrollArea-scrollbar {
|
& .mantine-ScrollArea-scrollbar {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--scrollbar-track-bg);
|
background: var(--scrollbar-track-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-ScrollArea-viewport > div {
|
& .mantine-ScrollArea-viewport > div {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string; windowBarStyle?: Platform }>`
|
const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string; windowBarStyle?: Platform }>`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: overlay !important;
|
overflow-y: overlay !important;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
margin-top: ${(props) =>
|
margin-top: ${(props) =>
|
||||||
props.windowBarStyle === Platform.WINDOWS ||
|
props.windowBarStyle === Platform.WINDOWS ||
|
||||||
props.windowBarStyle === Platform.MACOS ||
|
props.windowBarStyle === Platform.MACOS ||
|
||||||
props.windowBarStyle === Platform.LINUX
|
props.windowBarStyle === Platform.LINUX
|
||||||
? '0px'
|
? '0px'
|
||||||
: props.scrollBarOffset || '65px'};
|
: props.scrollBarOffset || '65px'};
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
margin-top: ${(props) =>
|
margin-top: ${(props) =>
|
||||||
props.windowBarStyle === Platform.WINDOWS ||
|
props.windowBarStyle === Platform.WINDOWS ||
|
||||||
props.windowBarStyle === Platform.MACOS ||
|
props.windowBarStyle === Platform.MACOS ||
|
||||||
props.windowBarStyle === Platform.LINUX
|
props.windowBarStyle === Platform.LINUX
|
||||||
? '0px'
|
? '0px'
|
||||||
: props.scrollBarOffset || '65px'};
|
: props.scrollBarOffset || '65px'};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
|
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
|
||||||
return (
|
return (
|
||||||
<StyledScrollArea
|
<StyledScrollArea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
scrollbarSize={12}
|
scrollbarSize={12}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledScrollArea>
|
</StyledScrollArea>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
interface NativeScrollAreaProps {
|
interface NativeScrollAreaProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
debugScrollPosition?: boolean;
|
debugScrollPosition?: boolean;
|
||||||
noHeader?: boolean;
|
noHeader?: boolean;
|
||||||
pageHeaderProps?: PageHeaderProps & { offset?: any; target?: any };
|
pageHeaderProps?: PageHeaderProps & { offset?: any; target?: any };
|
||||||
scrollBarOffset?: string;
|
scrollBarOffset?: string;
|
||||||
scrollHideDelay?: number;
|
scrollHideDelay?: number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NativeScrollArea = forwardRef(
|
export const NativeScrollArea = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
pageHeaderProps,
|
pageHeaderProps,
|
||||||
debugScrollPosition,
|
debugScrollPosition,
|
||||||
scrollBarOffset,
|
scrollBarOffset,
|
||||||
scrollHideDelay,
|
scrollHideDelay,
|
||||||
noHeader,
|
noHeader,
|
||||||
...props
|
...props
|
||||||
}: NativeScrollAreaProps,
|
}: NativeScrollAreaProps,
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const [hideScrollbar, setHideScrollbar] = useState(false);
|
const [hideScrollbar, setHideScrollbar] = useState(false);
|
||||||
const [hideHeader, setHideHeader] = useState(true);
|
const [hideHeader, setHideHeader] = useState(true);
|
||||||
const { start, clear } = useTimeout(
|
const { start, clear } = useTimeout(
|
||||||
() => setHideScrollbar(true),
|
() => setHideScrollbar(true),
|
||||||
scrollHideDelay !== undefined ? scrollHideDelay * 1000 : 0,
|
scrollHideDelay !== undefined ? scrollHideDelay * 1000 : 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const mergedRef = useMergedRef(ref, containerRef);
|
const mergedRef = useMergedRef(ref, containerRef);
|
||||||
|
|
||||||
const { scrollYProgress } = useScroll({
|
const { scrollYProgress } = useScroll({
|
||||||
container: containerRef,
|
container: containerRef,
|
||||||
offset: pageHeaderProps?.offset || ['center start', 'end start'],
|
offset: pageHeaderProps?.offset || ['center start', 'end start'],
|
||||||
target: pageHeaderProps?.target,
|
target: pageHeaderProps?.target,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatically hide the scrollbar after the timeout duration
|
// Automatically hide the scrollbar after the timeout duration
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
start();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const setHeaderVisibility = (v: number) => {
|
|
||||||
if (v === 1) {
|
|
||||||
return setHideHeader(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hideHeader === false) {
|
|
||||||
return setHideHeader(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const unsubscribe = scrollYProgress.on('change', setHeaderVisibility);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [hideHeader, scrollYProgress]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!noHeader && (
|
|
||||||
<PageHeader
|
|
||||||
isHidden={hideHeader}
|
|
||||||
position="absolute"
|
|
||||||
style={{ opacity: scrollYProgress as any }}
|
|
||||||
{...pageHeaderProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<StyledNativeScrollArea
|
|
||||||
ref={mergedRef}
|
|
||||||
className={hideScrollbar ? 'hide-scrollbar' : undefined}
|
|
||||||
scrollBarOffset={scrollBarOffset}
|
|
||||||
windowBarStyle={windowBarStyle}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setHideScrollbar(false);
|
|
||||||
clear();
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
start();
|
start();
|
||||||
}}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
{...props}
|
}, []);
|
||||||
>
|
|
||||||
{children}
|
useEffect(() => {
|
||||||
</StyledNativeScrollArea>
|
const setHeaderVisibility = (v: number) => {
|
||||||
{debugScrollPosition && (
|
if (v === 1) {
|
||||||
<motion.div
|
return setHideHeader(false);
|
||||||
style={{
|
}
|
||||||
background: 'red',
|
|
||||||
height: '10px',
|
if (hideHeader === false) {
|
||||||
left: 0,
|
return setHideHeader(true);
|
||||||
position: 'fixed',
|
}
|
||||||
right: 0,
|
|
||||||
scaleX: scrollYProgress,
|
return undefined;
|
||||||
top: 0,
|
};
|
||||||
transformOrigin: '0%',
|
|
||||||
width: '100%',
|
const unsubscribe = scrollYProgress.on('change', setHeaderVisibility);
|
||||||
zIndex: 5000,
|
|
||||||
}}
|
return () => {
|
||||||
/>
|
unsubscribe();
|
||||||
)}
|
};
|
||||||
</>
|
}, [hideHeader, scrollYProgress]);
|
||||||
);
|
|
||||||
},
|
return (
|
||||||
|
<>
|
||||||
|
{!noHeader && (
|
||||||
|
<PageHeader
|
||||||
|
isHidden={hideHeader}
|
||||||
|
position="absolute"
|
||||||
|
style={{ opacity: scrollYProgress as any }}
|
||||||
|
{...pageHeaderProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<StyledNativeScrollArea
|
||||||
|
ref={mergedRef}
|
||||||
|
className={hideScrollbar ? 'hide-scrollbar' : undefined}
|
||||||
|
scrollBarOffset={scrollBarOffset}
|
||||||
|
windowBarStyle={windowBarStyle}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHideScrollbar(false);
|
||||||
|
clear();
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
start();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledNativeScrollArea>
|
||||||
|
{debugScrollPosition && (
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
background: 'red',
|
||||||
|
height: '10px',
|
||||||
|
left: 0,
|
||||||
|
position: 'fixed',
|
||||||
|
right: 0,
|
||||||
|
scaleX: scrollYProgress,
|
||||||
|
top: 0,
|
||||||
|
transformOrigin: '0%',
|
||||||
|
width: '100%',
|
||||||
|
zIndex: 5000,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
@ -7,64 +7,64 @@ import { useSettingsStore } from '/@/renderer/store';
|
|||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
interface SearchInputProps extends TextInputProps {
|
interface SearchInputProps extends TextInputProps {
|
||||||
initialWidth?: number;
|
initialWidth?: number;
|
||||||
openedWidth?: number;
|
openedWidth?: number;
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchInput = ({
|
export const SearchInput = ({
|
||||||
initialWidth,
|
initialWidth,
|
||||||
onChange,
|
onChange,
|
||||||
openedWidth,
|
openedWidth,
|
||||||
...props
|
...props
|
||||||
}: SearchInputProps) => {
|
}: SearchInputProps) => {
|
||||||
const { ref, focused } = useFocusWithin();
|
const { ref, focused } = useFocusWithin();
|
||||||
const mergedRef = useMergedRef<HTMLInputElement>(ref);
|
const mergedRef = useMergedRef<HTMLInputElement>(ref);
|
||||||
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
|
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
|
||||||
|
|
||||||
const isOpened = focused || ref.current?.value;
|
const isOpened = focused || ref.current?.value;
|
||||||
const showIcon = !isOpened || (openedWidth || 100) > 100;
|
const showIcon = !isOpened || (openedWidth || 100) > 100;
|
||||||
|
|
||||||
useHotkeys([[binding.hotkey, () => ref.current.select()]]);
|
useHotkeys([[binding.hotkey, () => ref.current.select()]]);
|
||||||
|
|
||||||
const handleEscape = (e: KeyboardEvent<HTMLInputElement>) => {
|
const handleEscape = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
onChange?.({ target: { value: '' } } as ChangeEvent<HTMLInputElement>);
|
onChange?.({ target: { value: '' } } as ChangeEvent<HTMLInputElement>);
|
||||||
ref.current.value = '';
|
ref.current.value = '';
|
||||||
ref.current.blur();
|
ref.current.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={mergedRef}
|
ref={mergedRef}
|
||||||
{...props}
|
{...props}
|
||||||
icon={showIcon && <RiSearchLine />}
|
icon={showIcon && <RiSearchLine />}
|
||||||
rightSection={
|
rightSection={
|
||||||
isOpened ? (
|
isOpened ? (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
ref.current.value = '';
|
ref.current.value = '';
|
||||||
ref.current.focus();
|
ref.current.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
<RiCloseFill />
|
onChange={onChange}
|
||||||
</ActionIcon>
|
onKeyDown={handleEscape}
|
||||||
) : 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}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={handleEscape}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -6,37 +6,37 @@ import styled from 'styled-components';
|
|||||||
type SegmentedControlProps = MantineSegmentedControlProps;
|
type SegmentedControlProps = MantineSegmentedControlProps;
|
||||||
|
|
||||||
const StyledSegmentedControl = styled(MantineSegmentedControl)<MantineSegmentedControlProps>`
|
const StyledSegmentedControl = styled(MantineSegmentedControl)<MantineSegmentedControlProps>`
|
||||||
& .mantine-SegmentedControl-label {
|
& .mantine-SegmentedControl-label {
|
||||||
color: var(--input-fg);
|
color: var(--input-fg);
|
||||||
font-family: var(--content-font-family);
|
font-family: var(--content-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
|
|
||||||
& .mantine-SegmentedControl-disabled {
|
& .mantine-SegmentedControl-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-SegmentedControl-active {
|
& .mantine-SegmentedControl-active {
|
||||||
color: var(--input-active-fg);
|
color: var(--input-active-fg);
|
||||||
background-color: var(--input-active-bg);
|
background-color: var(--input-active-bg);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
export const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
||||||
({ ...props }: SegmentedControlProps, ref) => {
|
({ ...props }: SegmentedControlProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledSegmentedControl
|
<StyledSegmentedControl
|
||||||
ref={ref}
|
ref={ref}
|
||||||
styles={{}}
|
styles={{}}
|
||||||
transitionDuration={250}
|
transitionDuration={250}
|
||||||
transitionTimingFunction="linear"
|
transitionTimingFunction="linear"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,142 +1,142 @@
|
|||||||
import type {
|
import type {
|
||||||
SelectProps as MantineSelectProps,
|
SelectProps as MantineSelectProps,
|
||||||
MultiSelectProps as MantineMultiSelectProps,
|
MultiSelectProps as MantineMultiSelectProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Select as MantineSelect, MultiSelect as MantineMultiSelect } from '@mantine/core';
|
import { Select as MantineSelect, MultiSelect as MantineMultiSelect } from '@mantine/core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface SelectProps extends MantineSelectProps {
|
interface SelectProps extends MantineSelectProps {
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultiSelectProps extends MantineMultiSelectProps {
|
export interface MultiSelectProps extends MantineMultiSelectProps {
|
||||||
maxWidth?: number | string;
|
maxWidth?: number | string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledSelect = styled(MantineSelect)`
|
const StyledSelect = styled(MantineSelect)`
|
||||||
& [data-selected='true'] {
|
& [data-selected='true'] {
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Select-itemsWrapper {
|
& .mantine-Select-itemsWrapper {
|
||||||
& .mantine-Select-item {
|
& .mantine-Select-item {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
|
export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
withinPortal
|
withinPortal
|
||||||
styles={{
|
styles={{
|
||||||
dropdown: {
|
dropdown: {
|
||||||
background: 'var(--dropdown-menu-bg)',
|
background: 'var(--dropdown-menu-bg)',
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 20%))',
|
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 20%))',
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
background: 'var(--input-bg)',
|
background: 'var(--input-bg)',
|
||||||
color: 'var(--input-fg)',
|
color: 'var(--input-fg)',
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
background: 'var(--dropdown-menu-bg-hover)',
|
||||||
},
|
},
|
||||||
'&[data-hovered]': {
|
'&[data-hovered]': {
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
background: 'var(--dropdown-menu-bg-hover)',
|
||||||
},
|
},
|
||||||
'&[data-selected="true"]': {
|
'&[data-selected="true"]': {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
background: 'var(--dropdown-menu-bg-hover)',
|
||||||
},
|
},
|
||||||
background: 'none',
|
background: 'none',
|
||||||
color: 'var(--primary-color)',
|
color: 'var(--primary-color)',
|
||||||
},
|
},
|
||||||
color: 'var(--dropdown-menu-fg)',
|
color: 'var(--dropdown-menu-fg)',
|
||||||
padding: '.3rem',
|
padding: '.3rem',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
transitionProps={{ duration: 100, transition: 'fade' }}
|
transitionProps={{ duration: 100, transition: 'fade' }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledMultiSelect = styled(MantineMultiSelect)`
|
const StyledMultiSelect = styled(MantineMultiSelect)`
|
||||||
& [data-selected='true'] {
|
& [data-selected='true'] {
|
||||||
background: var(--input-select-bg);
|
background: var(--input-select-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& [data-disabled='true'] {
|
& [data-disabled='true'] {
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-MultiSelect-itemsWrapper {
|
& .mantine-MultiSelect-itemsWrapper {
|
||||||
& .mantine-Select-item {
|
& .mantine-Select-item {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) => {
|
export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledMultiSelect
|
<StyledMultiSelect
|
||||||
withinPortal
|
withinPortal
|
||||||
styles={{
|
styles={{
|
||||||
dropdown: {
|
dropdown: {
|
||||||
background: 'var(--dropdown-menu-bg)',
|
background: 'var(--dropdown-menu-bg)',
|
||||||
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 20%))',
|
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 20%))',
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
background: 'var(--input-bg)',
|
background: 'var(--input-bg)',
|
||||||
color: 'var(--input-fg)',
|
color: 'var(--input-fg)',
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
background: 'var(--dropdown-menu-bg-hover)',
|
||||||
},
|
},
|
||||||
'&[data-hovered]': {
|
'&[data-hovered]': {
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
background: 'var(--dropdown-menu-bg-hover)',
|
||||||
},
|
},
|
||||||
'&[data-selected="true"]': {
|
'&[data-selected="true"]': {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'var(--dropdown-menu-bg-hover)',
|
background: 'var(--dropdown-menu-bg-hover)',
|
||||||
},
|
},
|
||||||
background: 'none',
|
background: 'none',
|
||||||
color: 'var(--primary-color)',
|
color: 'var(--primary-color)',
|
||||||
},
|
},
|
||||||
color: 'var(--dropdown-menu-fg)',
|
color: 'var(--dropdown-menu-fg)',
|
||||||
padding: '.5rem .1rem',
|
padding: '.5rem .1rem',
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
margin: '.2rem',
|
margin: '.2rem',
|
||||||
paddingBottom: '1rem',
|
paddingBottom: '1rem',
|
||||||
paddingLeft: '1rem',
|
paddingLeft: '1rem',
|
||||||
paddingTop: '1rem',
|
paddingTop: '1rem',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
sx={{ maxWidth, width }}
|
sx={{ maxWidth, width }}
|
||||||
transitionProps={{ duration: 100, transition: 'fade' }}
|
transitionProps={{ duration: 100, transition: 'fade' }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Select.defaultProps = {
|
Select.defaultProps = {
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
MultiSelect.defaultProps = {
|
MultiSelect.defaultProps = {
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
};
|
};
|
||||||
|
@ -3,16 +3,16 @@ import { Skeleton as MantineSkeleton } from '@mantine/core';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledSkeleton = styled(MantineSkeleton)`
|
const StyledSkeleton = styled(MantineSkeleton)`
|
||||||
&::after {
|
&::after {
|
||||||
background: var(--placeholder-bg);
|
background: var(--placeholder-bg);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Skeleton = ({ ...props }: MantineSkeletonProps) => {
|
export const Skeleton = ({ ...props }: MantineSkeletonProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledSkeleton
|
<StyledSkeleton
|
||||||
animate={false}
|
animate={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,30 +5,30 @@ import styled from 'styled-components';
|
|||||||
type SliderProps = MantineSliderProps;
|
type SliderProps = MantineSliderProps;
|
||||||
|
|
||||||
const StyledSlider = styled(MantineSlider)`
|
const StyledSlider = styled(MantineSlider)`
|
||||||
& .mantine-Slider-track {
|
& .mantine-Slider-track {
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
background-color: var(--slider-track-bg);
|
background-color: var(--slider-track-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Slider-bar {
|
& .mantine-Slider-bar {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Slider-thumb {
|
& .mantine-Slider-thumb {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
background: var(--slider-thumb-bg);
|
background: var(--slider-thumb-bg);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .mantine-Slider-label {
|
& .mantine-Slider-label {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
color: var(--tooltip-fg);
|
color: var(--tooltip-fg);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
background: var(--tooltip-bg);
|
background: var(--tooltip-bg);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Slider = ({ ...props }: SliderProps) => {
|
export const Slider = ({ ...props }: SliderProps) => {
|
||||||
return <StyledSlider {...props} />;
|
return <StyledSlider {...props} />;
|
||||||
};
|
};
|
||||||
|
@ -5,35 +5,35 @@ import styled from 'styled-components';
|
|||||||
import { rotating } from '/@/renderer/styles';
|
import { rotating } from '/@/renderer/styles';
|
||||||
|
|
||||||
interface SpinnerProps extends IconType {
|
interface SpinnerProps extends IconType {
|
||||||
color?: string;
|
color?: string;
|
||||||
container?: boolean;
|
container?: boolean;
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpinnerIcon = styled(RiLoader5Fill)`
|
export const SpinnerIcon = styled(RiLoader5Fill)`
|
||||||
${rotating}
|
${rotating}
|
||||||
animation: rotating 1s ease-in-out infinite;
|
animation: rotating 1s ease-in-out infinite;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Spinner = ({ ...props }: SpinnerProps) => {
|
export const Spinner = ({ ...props }: SpinnerProps) => {
|
||||||
if (props.container) {
|
if (props.container) {
|
||||||
return (
|
return (
|
||||||
<Center
|
<Center
|
||||||
h="100%"
|
h="100%"
|
||||||
w="100%"
|
w="100%"
|
||||||
>
|
>
|
||||||
<SpinnerIcon
|
<SpinnerIcon
|
||||||
color={props.color}
|
color={props.color}
|
||||||
size={props.size}
|
size={props.size}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SpinnerIcon {...props} />;
|
return <SpinnerIcon {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
Spinner.defaultProps = {
|
Spinner.defaultProps = {
|
||||||
color: undefined,
|
color: undefined,
|
||||||
size: 15,
|
size: 15,
|
||||||
};
|
};
|
||||||
|
@ -5,24 +5,24 @@ import styled from 'styled-components';
|
|||||||
type SwitchProps = MantineSwitchProps;
|
type SwitchProps = MantineSwitchProps;
|
||||||
|
|
||||||
const StyledSwitch = styled(MantineSwitch)`
|
const StyledSwitch = styled(MantineSwitch)`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
& .mantine-Switch-track {
|
& .mantine-Switch-track {
|
||||||
background-color: var(--switch-track-bg);
|
background-color: var(--switch-track-bg);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Switch-input {
|
|
||||||
&:checked + .mantine-Switch-track {
|
|
||||||
background-color: var(--switch-track-enabled-bg);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& .mantine-Switch-thumb {
|
& .mantine-Switch-input {
|
||||||
background-color: var(--switch-thumb-bg);
|
&: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) => {
|
export const Switch = ({ ...props }: SwitchProps) => {
|
||||||
return <StyledSwitch {...props} />;
|
return <StyledSwitch {...props} />;
|
||||||
};
|
};
|
||||||
|
@ -5,57 +5,57 @@ import styled from 'styled-components';
|
|||||||
type TabsProps = MantineTabsProps;
|
type TabsProps = MantineTabsProps;
|
||||||
|
|
||||||
const StyledTabs = styled(MantineTabs)`
|
const StyledTabs = styled(MantineTabs)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
& .mantine-Tabs-tabsList {
|
& .mantine-Tabs-tabsList {
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
|
||||||
|
|
||||||
&.mantine-Tabs-tab {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
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;
|
&.mantine-Tabs-tab {
|
||||||
}
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
button[data-active] {
|
font-size: 1rem;
|
||||||
color: var(--btn-subtle-fg);
|
background-color: var(--main-bg);
|
||||||
background: none;
|
}
|
||||||
border-color: var(--primary-color);
|
|
||||||
|
& .mantine-Tabs-panel {
|
||||||
&:hover {
|
padding: 1.5rem 0.5rem;
|
||||||
background: none;
|
}
|
||||||
|
|
||||||
|
& .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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tabs = ({ children, ...props }: TabsProps) => {
|
export const Tabs = ({ children, ...props }: TabsProps) => {
|
||||||
return <StyledTabs {...props}>{children}</StyledTabs>;
|
return <StyledTabs {...props}>{children}</StyledTabs>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Panel = ({ children, ...props }: TabsPanelProps) => {
|
const Panel = ({ children, ...props }: TabsPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledTabs.Panel {...props}>
|
<StyledTabs.Panel {...props}>
|
||||||
<Suspense fallback={<></>}>{children}</Suspense>
|
<Suspense fallback={<></>}>{children}</Suspense>
|
||||||
</StyledTabs.Panel>
|
</StyledTabs.Panel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tabs.List = StyledTabs.List;
|
Tabs.List = StyledTabs.List;
|
||||||
|
@ -7,50 +7,50 @@ import { textEllipsis } from '/@/renderer/styles';
|
|||||||
type MantineTextTitleDivProps = MantineTitleProps & ComponentPropsWithoutRef<'div'>;
|
type MantineTextTitleDivProps = MantineTitleProps & ComponentPropsWithoutRef<'div'>;
|
||||||
|
|
||||||
interface TextTitleProps extends MantineTextTitleDivProps {
|
interface TextTitleProps extends MantineTextTitleDivProps {
|
||||||
$link?: boolean;
|
$link?: boolean;
|
||||||
$noSelect?: boolean;
|
$noSelect?: boolean;
|
||||||
$secondary?: boolean;
|
$secondary?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
overflow?: 'hidden' | 'visible';
|
overflow?: 'hidden' | 'visible';
|
||||||
to?: string;
|
to?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
|
const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
|
||||||
overflow: ${(props) => props.overflow};
|
overflow: ${(props) => props.overflow};
|
||||||
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||||
cursor: ${(props) => props.$link && 'cursor'};
|
cursor: ${(props) => props.$link && 'cursor'};
|
||||||
transition: color 0.2s ease-in-out;
|
transition: color 0.2s ease-in-out;
|
||||||
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
||||||
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
|
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.$link && 'var(--main-fg)'};
|
color: ${(props) => props.$link && 'var(--main-fg)'};
|
||||||
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
|
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const _TextTitle = ({ children, $secondary, overflow, $noSelect, ...rest }: TextTitleProps) => {
|
const _TextTitle = ({ children, $secondary, overflow, $noSelect, ...rest }: TextTitleProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledTextTitle
|
<StyledTextTitle
|
||||||
$noSelect={$noSelect}
|
$noSelect={$noSelect}
|
||||||
$secondary={$secondary}
|
$secondary={$secondary}
|
||||||
overflow={overflow}
|
overflow={overflow}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledTextTitle>
|
</StyledTextTitle>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextTitle = createPolymorphicComponent<'div', TextTitleProps>(_TextTitle);
|
export const TextTitle = createPolymorphicComponent<'div', TextTitleProps>(_TextTitle);
|
||||||
|
|
||||||
_TextTitle.defaultProps = {
|
_TextTitle.defaultProps = {
|
||||||
$link: false,
|
$link: false,
|
||||||
$noSelect: false,
|
$noSelect: false,
|
||||||
$secondary: false,
|
$secondary: false,
|
||||||
font: undefined,
|
font: undefined,
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
to: '',
|
to: '',
|
||||||
weight: 400,
|
weight: 400,
|
||||||
};
|
};
|
||||||
|
@ -8,52 +8,52 @@ import { textEllipsis } from '/@/renderer/styles';
|
|||||||
type MantineTextDivProps = MantineTextProps & ComponentPropsWithoutRef<'div'>;
|
type MantineTextDivProps = MantineTextProps & ComponentPropsWithoutRef<'div'>;
|
||||||
|
|
||||||
interface TextProps extends MantineTextDivProps {
|
interface TextProps extends MantineTextDivProps {
|
||||||
$link?: boolean;
|
$link?: boolean;
|
||||||
$noSelect?: boolean;
|
$noSelect?: boolean;
|
||||||
$secondary?: boolean;
|
$secondary?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
font?: Font;
|
font?: Font;
|
||||||
overflow?: 'hidden' | 'visible';
|
overflow?: 'hidden' | 'visible';
|
||||||
to?: string;
|
to?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledText = styled(MantineText)<TextProps>`
|
const StyledText = styled(MantineText)<TextProps>`
|
||||||
overflow: ${(props) => props.overflow};
|
overflow: ${(props) => props.overflow};
|
||||||
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
color: ${(props) => (props.$secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||||
font-family: ${(props) => props.font};
|
font-family: ${(props) => props.font};
|
||||||
cursor: ${(props) => props.$link && 'cursor'};
|
cursor: ${(props) => props.$link && 'cursor'};
|
||||||
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
||||||
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
|
${(props) => props.overflow === 'hidden' && !props.lineClamp && textEllipsis}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.$link && 'var(--main-fg)'};
|
color: ${(props) => props.$link && 'var(--main-fg)'};
|
||||||
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
|
text-decoration: ${(props) => (props.$link ? 'underline' : 'none')};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const _Text = ({ children, $secondary, overflow, font, $noSelect, ...rest }: TextProps) => {
|
export const _Text = ({ children, $secondary, overflow, font, $noSelect, ...rest }: TextProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledText
|
<StyledText
|
||||||
$noSelect={$noSelect}
|
$noSelect={$noSelect}
|
||||||
$secondary={$secondary}
|
$secondary={$secondary}
|
||||||
font={font}
|
font={font}
|
||||||
overflow={overflow}
|
overflow={overflow}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledText>
|
</StyledText>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Text = createPolymorphicComponent<'div', TextProps>(_Text);
|
export const Text = createPolymorphicComponent<'div', TextProps>(_Text);
|
||||||
|
|
||||||
_Text.defaultProps = {
|
_Text.defaultProps = {
|
||||||
$link: false,
|
$link: false,
|
||||||
$noSelect: false,
|
$noSelect: false,
|
||||||
$secondary: false,
|
$secondary: false,
|
||||||
font: undefined,
|
font: undefined,
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
to: '',
|
to: '',
|
||||||
weight: 400,
|
weight: 400,
|
||||||
};
|
};
|
||||||
|
@ -1,76 +1,76 @@
|
|||||||
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
|
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
showNotification,
|
showNotification,
|
||||||
updateNotification,
|
updateNotification,
|
||||||
hideNotification,
|
hideNotification,
|
||||||
cleanNotifications,
|
cleanNotifications,
|
||||||
cleanNotificationsQueue,
|
cleanNotificationsQueue,
|
||||||
} from '@mantine/notifications';
|
} from '@mantine/notifications';
|
||||||
|
|
||||||
interface NotificationProps extends MantineNotificationProps {
|
interface NotificationProps extends MantineNotificationProps {
|
||||||
type?: 'success' | 'error' | 'warning' | 'info';
|
type?: 'success' | 'error' | 'warning' | 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
const showToast = ({ type, ...props }: NotificationProps) => {
|
const showToast = ({ type, ...props }: NotificationProps) => {
|
||||||
const color =
|
const color =
|
||||||
type === 'success'
|
type === 'success'
|
||||||
? 'var(--success-color)'
|
? 'var(--success-color)'
|
||||||
: type === 'warning'
|
: type === 'warning'
|
||||||
? 'var(--warning-color)'
|
? 'var(--warning-color)'
|
||||||
: type === 'error'
|
: type === 'error'
|
||||||
? 'var(--danger-color)'
|
? 'var(--danger-color)'
|
||||||
: 'var(--primary-color)';
|
: 'var(--primary-color)';
|
||||||
|
|
||||||
const defaultTitle =
|
const defaultTitle =
|
||||||
type === 'success'
|
type === 'success'
|
||||||
? 'Success'
|
? 'Success'
|
||||||
: type === 'warning'
|
: type === 'warning'
|
||||||
? 'Warning'
|
? 'Warning'
|
||||||
: type === 'error'
|
: type === 'error'
|
||||||
? 'Error'
|
? 'Error'
|
||||||
: 'Info';
|
: 'Info';
|
||||||
|
|
||||||
const defaultDuration = type === 'error' ? 2000 : 1000;
|
const defaultDuration = type === 'error' ? 2000 : 1000;
|
||||||
|
|
||||||
return showNotification({
|
return showNotification({
|
||||||
autoClose: defaultDuration,
|
autoClose: defaultDuration,
|
||||||
styles: () => ({
|
styles: () => ({
|
||||||
closeButton: {
|
closeButton: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
color: 'var(--toast-description-fg)',
|
color: 'var(--toast-description-fg)',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
},
|
},
|
||||||
loader: {
|
loader: {
|
||||||
margin: '1rem',
|
margin: '1rem',
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
'&::before': { backgroundColor: color },
|
'&::before': { backgroundColor: color },
|
||||||
background: 'var(--toast-bg)',
|
background: 'var(--toast-bg)',
|
||||||
border: '2px solid var(--generic-border-color)',
|
border: '2px solid var(--generic-border-color)',
|
||||||
bottom: '90px',
|
bottom: '90px',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: 'var(--toast-title-fg)',
|
color: 'var(--toast-title-fg)',
|
||||||
fontSize: '1.3rem',
|
fontSize: '1.3rem',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
title: defaultTitle,
|
title: defaultTitle,
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toast = {
|
export const toast = {
|
||||||
clean: cleanNotifications,
|
clean: cleanNotifications,
|
||||||
cleanQueue: cleanNotificationsQueue,
|
cleanQueue: cleanNotificationsQueue,
|
||||||
error: (props: NotificationProps) => showToast({ type: 'error', ...props }),
|
error: (props: NotificationProps) => showToast({ type: 'error', ...props }),
|
||||||
hide: hideNotification,
|
hide: hideNotification,
|
||||||
info: (props: NotificationProps) => showToast({ type: 'info', ...props }),
|
info: (props: NotificationProps) => showToast({ type: 'info', ...props }),
|
||||||
show: showToast,
|
show: showToast,
|
||||||
success: (props: NotificationProps) => showToast({ type: 'success', ...props }),
|
success: (props: NotificationProps) => showToast({ type: 'success', ...props }),
|
||||||
update: updateNotification,
|
update: updateNotification,
|
||||||
warn: (props: NotificationProps) => showToast({ type: 'warning', ...props }),
|
warn: (props: NotificationProps) => showToast({ type: 'warning', ...props }),
|
||||||
};
|
};
|
||||||
|
@ -3,44 +3,44 @@ import { Tooltip as MantineTooltip } from '@mantine/core';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledTooltip = styled(MantineTooltip)`
|
const StyledTooltip = styled(MantineTooltip)`
|
||||||
& .mantine-Tooltip-tooltip {
|
& .mantine-Tooltip-tooltip {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledTooltip
|
<StyledTooltip
|
||||||
multiline
|
multiline
|
||||||
withinPortal
|
withinPortal
|
||||||
pl={10}
|
pl={10}
|
||||||
pr={10}
|
pr={10}
|
||||||
py={5}
|
py={5}
|
||||||
radius="xs"
|
radius="xs"
|
||||||
styles={{
|
styles={{
|
||||||
tooltip: {
|
tooltip: {
|
||||||
background: 'var(--tooltip-bg)',
|
background: 'var(--tooltip-bg)',
|
||||||
boxShadow: '4px 4px 10px 0px rgba(0,0,0,0.2)',
|
boxShadow: '4px 4px 10px 0px rgba(0,0,0,0.2)',
|
||||||
color: 'var(--tooltip-fg)',
|
color: 'var(--tooltip-fg)',
|
||||||
fontSize: '1.1rem',
|
fontSize: '1.1rem',
|
||||||
fontWeight: 550,
|
fontWeight: 550,
|
||||||
maxWidth: '250px',
|
maxWidth: '250px',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
transitionProps={{
|
transitionProps={{
|
||||||
duration: 250,
|
duration: 250,
|
||||||
transition: 'fade',
|
transition: 'fade',
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledTooltip>
|
</StyledTooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.defaultProps = {
|
Tooltip.defaultProps = {
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
withArrow: true,
|
withArrow: true,
|
||||||
withinPortal: true,
|
withinPortal: true,
|
||||||
};
|
};
|
||||||
|
@ -11,87 +11,91 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
|
|||||||
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
interface BaseGridCardProps {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
controls: {
|
controls: {
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
handleFavorite: (options: {
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
id: string[];
|
||||||
itemType: LibraryItem;
|
isFavorite: boolean;
|
||||||
playButtonBehavior: Play;
|
itemType: LibraryItem;
|
||||||
route: CardRoute;
|
}) => void;
|
||||||
};
|
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||||
data: any;
|
itemType: LibraryItem;
|
||||||
isHidden?: boolean;
|
playButtonBehavior: Play;
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
route: CardRoute;
|
||||||
|
};
|
||||||
|
data: any;
|
||||||
|
isHidden?: boolean;
|
||||||
|
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>`
|
const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 2rem);
|
height: calc(100% - 2rem);
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--card-default-bg);
|
background: var(--card-default-bg);
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--card-default-bg-hover);
|
background: var(--card-default-bg-hover);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InnerCardContainer = styled.div`
|
const InnerCardContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.card-controls {
|
.card-controls {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .card-controls {
|
&:hover .card-controls {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover * {
|
&:hover * {
|
||||||
&::before {
|
&::before {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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%;
|
height: 100%;
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
aspect-ratio: 1/1;
|
||||||
opacity: 0;
|
overflow: hidden;
|
||||||
transition: all 0.2s ease-in-out;
|
background: var(--placeholder-bg);
|
||||||
content: '';
|
border-radius: var(--card-default-radius);
|
||||||
user-select: none;
|
|
||||||
}
|
&::before {
|
||||||
${(props) =>
|
position: absolute;
|
||||||
props.$isFavorite &&
|
top: 0;
|
||||||
`
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 100%) 35%, rgba(0, 0, 0, 0%) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
content: '';
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
${(props) =>
|
||||||
|
props.$isFavorite &&
|
||||||
|
`
|
||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -50px;
|
top: -50px;
|
||||||
@ -108,134 +112,134 @@ const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled(SimpleImg)`
|
const Image = styled(SimpleImg)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DetailContainer = styled.div`
|
const DetailContainer = styled.div`
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DefaultCard = ({
|
export const DefaultCard = ({
|
||||||
listChildProps,
|
listChildProps,
|
||||||
data,
|
data,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
controls,
|
controls,
|
||||||
isHidden,
|
isHidden,
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const path = generatePath(
|
const path = generatePath(
|
||||||
controls.route.route,
|
controls.route.route,
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Placeholder = RiAlbumFill;
|
let Placeholder = RiAlbumFill;
|
||||||
|
|
||||||
switch (controls.itemType) {
|
switch (controls.itemType) {
|
||||||
case LibraryItem.ALBUM:
|
case LibraryItem.ALBUM:
|
||||||
Placeholder = RiAlbumFill;
|
Placeholder = RiAlbumFill;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ARTIST:
|
case LibraryItem.ARTIST:
|
||||||
Placeholder = RiUserVoiceFill;
|
Placeholder = RiUserVoiceFill;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
Placeholder = RiUserVoiceFill;
|
Placeholder = RiUserVoiceFill;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.PLAYLIST:
|
case LibraryItem.PLAYLIST:
|
||||||
Placeholder = RiPlayListFill;
|
Placeholder = RiPlayListFill;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Placeholder = RiAlbumFill;
|
Placeholder = RiAlbumFill;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultCardContainer
|
||||||
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
|
onClick={() => navigate(path)}
|
||||||
|
>
|
||||||
|
<InnerCardContainer>
|
||||||
|
<ImageContainer $isFavorite={data?.userFavorite}>
|
||||||
|
{data?.imageUrl ? (
|
||||||
|
<Image
|
||||||
|
importance="auto"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<GridCardControls
|
||||||
|
handleFavorite={controls.handleFavorite}
|
||||||
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
itemData={data}
|
||||||
|
itemType={controls.itemType}
|
||||||
|
/>
|
||||||
|
</ImageContainer>
|
||||||
|
<DetailContainer>
|
||||||
|
<CardRows
|
||||||
|
data={data}
|
||||||
|
rows={controls.cardRows}
|
||||||
|
/>
|
||||||
|
</DetailContainer>
|
||||||
|
</InnerCardContainer>
|
||||||
|
</DefaultCardContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultCardContainer
|
<DefaultCardContainer
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
onClick={() => navigate(path)}
|
$isHidden={isHidden}
|
||||||
>
|
>
|
||||||
<InnerCardContainer>
|
<InnerCardContainer>
|
||||||
<ImageContainer $isFavorite={data?.userFavorite}>
|
<ImageContainer>
|
||||||
{data?.imageUrl ? (
|
<Skeleton
|
||||||
<Image
|
visible
|
||||||
importance="auto"
|
radius="sm"
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
/>
|
||||||
src={data?.imageUrl}
|
</ImageContainer>
|
||||||
/>
|
<DetailContainer>
|
||||||
) : (
|
<Stack spacing="sm">
|
||||||
<Center
|
{controls.cardRows.map((row, index) => (
|
||||||
sx={{
|
<Skeleton
|
||||||
background: 'var(--placeholder-bg)',
|
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
||||||
borderRadius: 'var(--card-default-radius)',
|
visible
|
||||||
height: '100%',
|
height={14}
|
||||||
width: '100%',
|
radius="sm"
|
||||||
}}
|
/>
|
||||||
>
|
))}
|
||||||
<Placeholder
|
</Stack>
|
||||||
color="var(--placeholder-fg)"
|
</DetailContainer>
|
||||||
size={35}
|
</InnerCardContainer>
|
||||||
/>
|
</DefaultCardContainer>
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<GridCardControls
|
|
||||||
handleFavorite={controls.handleFavorite}
|
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
|
||||||
itemData={data}
|
|
||||||
itemType={controls.itemType}
|
|
||||||
/>
|
|
||||||
</ImageContainer>
|
|
||||||
<DetailContainer>
|
|
||||||
<CardRows
|
|
||||||
data={data}
|
|
||||||
rows={controls.cardRows}
|
|
||||||
/>
|
|
||||||
</DetailContainer>
|
|
||||||
</InnerCardContainer>
|
|
||||||
</DefaultCardContainer>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DefaultCardContainer
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
|
||||||
$isHidden={isHidden}
|
|
||||||
>
|
|
||||||
<InnerCardContainer>
|
|
||||||
<ImageContainer>
|
|
||||||
<Skeleton
|
|
||||||
visible
|
|
||||||
radius="sm"
|
|
||||||
/>
|
|
||||||
</ImageContainer>
|
|
||||||
<DetailContainer>
|
|
||||||
<Stack spacing="sm">
|
|
||||||
{controls.cardRows.map((row, index) => (
|
|
||||||
<Skeleton
|
|
||||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
|
||||||
visible
|
|
||||||
height={14}
|
|
||||||
radius="sm"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</DetailContainer>
|
|
||||||
</InnerCardContainer>
|
|
||||||
</DefaultCardContainer>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -9,197 +9,197 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
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 {
|
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';
|
||||||
|
|
||||||
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
||||||
|
|
||||||
const PlayButton = styled.button<PlayButtonType>`
|
const PlayButton = styled.button<PlayButtonType>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: rgb(255, 255, 255);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
transition: scale 0.1s ease-in-out;
|
transition: scale 0.1s ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1.1;
|
scale: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1;
|
scale: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: rgb(0, 0, 0);
|
fill: rgb(0, 0, 0);
|
||||||
stroke: rgb(0, 0, 0);
|
stroke: rgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SecondaryButton = styled(_Button)`
|
const SecondaryButton = styled(_Button)`
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
transition: scale 0.2s linear;
|
transition: scale 0.2s linear;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1.1;
|
scale: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1;
|
scale: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GridCardControlsContainer = styled.div<{ $isFavorite?: boolean }>`
|
const GridCardControlsContainer = styled.div<{ $isFavorite?: boolean }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FavoriteBanner = styled.div`
|
const FavoriteBanner = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -50px;
|
top: -50px;
|
||||||
left: -50px;
|
left: -50px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
box-shadow: 0 0 10px 8px rgba(0, 0, 0, 80%);
|
box-shadow: 0 0 10px 8px rgba(0, 0, 0, 80%);
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
content: '';
|
content: '';
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ControlsRow = styled.div`
|
const ControlsRow = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% / 3);
|
height: calc(100% / 3);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BottomControls = styled(ControlsRow)`
|
const BottomControls = styled(ControlsRow)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GridCardControls = ({
|
export const GridCardControls = ({
|
||||||
itemData,
|
itemData,
|
||||||
itemType,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
handleFavorite,
|
|
||||||
}: {
|
|
||||||
handleFavorite: (options: {
|
|
||||||
id: string[];
|
|
||||||
isFavorite: boolean;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
serverId: string;
|
|
||||||
}) => void;
|
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
|
||||||
itemData: any;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
}) => {
|
|
||||||
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: [itemData.id],
|
|
||||||
type: itemType,
|
|
||||||
},
|
|
||||||
playType: playType || playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
handleFavorite?.({
|
|
||||||
id: [itemData.id],
|
|
||||||
isFavorite: itemData.userFavorite,
|
|
||||||
itemType,
|
|
||||||
serverId,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsFavorite(!isFavorite);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContextMenu = useHandleGeneralContextMenu(
|
|
||||||
itemType,
|
itemType,
|
||||||
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
|
handlePlayQueueAdd,
|
||||||
);
|
handleFavorite,
|
||||||
|
}: {
|
||||||
|
handleFavorite: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
serverId: string;
|
||||||
|
}) => void;
|
||||||
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
|
itemData: any;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => {
|
||||||
|
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
return (
|
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||||
<>
|
e.preventDefault();
|
||||||
{isFavorite ? <FavoriteBanner /> : null}
|
e.stopPropagation();
|
||||||
<GridCardControlsContainer
|
|
||||||
$isFavorite
|
handlePlayQueueAdd?.({
|
||||||
className="card-controls"
|
byItemType: {
|
||||||
>
|
id: [itemData.id],
|
||||||
<PlayButton onClick={handlePlay}>
|
type: itemType,
|
||||||
<RiPlayFill size={25} />
|
},
|
||||||
</PlayButton>
|
playType: playType || playButtonBehavior,
|
||||||
<BottomControls>
|
});
|
||||||
<SecondaryButton
|
};
|
||||||
p={5}
|
|
||||||
variant="subtle"
|
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
|
||||||
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
e.preventDefault();
|
||||||
>
|
e.stopPropagation();
|
||||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
|
||||||
{isFavorite ? (
|
handleFavorite?.({
|
||||||
<RiHeartFill size={20} />
|
id: [itemData.id],
|
||||||
) : (
|
isFavorite: itemData.userFavorite,
|
||||||
<RiHeartLine
|
itemType,
|
||||||
color="white"
|
serverId,
|
||||||
size={20}
|
});
|
||||||
/>
|
|
||||||
)}
|
setIsFavorite(!isFavorite);
|
||||||
</FavoriteWrapper>
|
};
|
||||||
</SecondaryButton>
|
|
||||||
<SecondaryButton
|
const handleContextMenu = useHandleGeneralContextMenu(
|
||||||
p={5}
|
itemType,
|
||||||
variant="subtle"
|
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
|
||||||
onClick={(e) => {
|
);
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
return (
|
||||||
handleContextMenu(e, [itemData]);
|
<>
|
||||||
}}
|
{isFavorite ? <FavoriteBanner /> : null}
|
||||||
>
|
<GridCardControlsContainer
|
||||||
<RiMoreFill
|
$isFavorite
|
||||||
color="white"
|
className="card-controls"
|
||||||
size={20}
|
>
|
||||||
/>
|
<PlayButton onClick={handlePlay}>
|
||||||
</SecondaryButton>
|
<RiPlayFill size={25} />
|
||||||
</BottomControls>
|
</PlayButton>
|
||||||
</GridCardControlsContainer>
|
<BottomControls>
|
||||||
</>
|
<SecondaryButton
|
||||||
);
|
p={5}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
||||||
|
>
|
||||||
|
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
||||||
|
{isFavorite ? (
|
||||||
|
<RiHeartFill size={20} />
|
||||||
|
) : (
|
||||||
|
<RiHeartLine
|
||||||
|
color="white"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FavoriteWrapper>
|
||||||
|
</SecondaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
p={5}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleContextMenu(e, [itemData]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiMoreFill
|
||||||
|
color="white"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</SecondaryButton>
|
||||||
|
</BottomControls>
|
||||||
|
</GridCardControlsContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,58 +6,58 @@ import { PosterCard } from '/@/renderer/components/virtual-grid/grid-card/poster
|
|||||||
import { GridCardData, ListDisplayType } from '/@/renderer/types';
|
import { GridCardData, ListDisplayType } from '/@/renderer/types';
|
||||||
|
|
||||||
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
|
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
|
||||||
const {
|
const {
|
||||||
columnCount,
|
columnCount,
|
||||||
itemCount,
|
itemCount,
|
||||||
cardRows,
|
cardRows,
|
||||||
itemData,
|
itemData,
|
||||||
itemType,
|
itemType,
|
||||||
playButtonBehavior,
|
playButtonBehavior,
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
handleFavorite,
|
handleFavorite,
|
||||||
route,
|
route,
|
||||||
display,
|
display,
|
||||||
} = data as GridCardData;
|
} = data as GridCardData;
|
||||||
|
|
||||||
const cards = [];
|
const cards = [];
|
||||||
const startIndex = index * columnCount;
|
const startIndex = index * columnCount;
|
||||||
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
||||||
|
|
||||||
const columnCountInRow = stopIndex - startIndex + 1;
|
const columnCountInRow = stopIndex - startIndex + 1;
|
||||||
let columnCountToAdd = 0;
|
let columnCountToAdd = 0;
|
||||||
if (columnCountInRow !== columnCount) {
|
if (columnCountInRow !== columnCount) {
|
||||||
columnCountToAdd = columnCount - columnCountInRow;
|
columnCountToAdd = columnCount - columnCountInRow;
|
||||||
}
|
}
|
||||||
const View = display === ListDisplayType.CARD ? DefaultCard : PosterCard;
|
const View = display === ListDisplayType.CARD ? DefaultCard : PosterCard;
|
||||||
|
|
||||||
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
|
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
|
||||||
cards.push(
|
cards.push(
|
||||||
<View
|
<View
|
||||||
key={`card-${i}-${index}`}
|
key={`card-${i}-${index}`}
|
||||||
columnIndex={i}
|
columnIndex={i}
|
||||||
controls={{
|
controls={{
|
||||||
cardRows,
|
cardRows,
|
||||||
handleFavorite,
|
handleFavorite,
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
itemType,
|
itemType,
|
||||||
playButtonBehavior,
|
playButtonBehavior,
|
||||||
route,
|
route,
|
||||||
}}
|
}}
|
||||||
data={itemData[i]}
|
data={itemData[i]}
|
||||||
isHidden={i > stopIndex}
|
isHidden={i > stopIndex}
|
||||||
listChildProps={{ index }}
|
listChildProps={{ index }}
|
||||||
/>,
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cards}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cards}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, areEqual);
|
}, areEqual);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user