mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-04 01:30:01 +02:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21d788226c | |||
| 9a1bf8f4a9 | |||
| 0fab3ba318 | |||
| 5ddbfcbfee | |||
| b6519e9839 | |||
| 2103c3b8c6 | |||
| 4a7f084b59 | |||
| 5ef4744c44 | |||
| ecda4ef8bc | |||
| 0ed68e8ebb | |||
| 417365f091 | |||
| ff8a21af08 | |||
| ff426bda6d | |||
| 0664b0ad02 | |||
| 61cc87e0b7 | |||
| 36624350f6 | |||
| dbe46e03a4 | |||
| 16e00a0f9f | |||
| 00d9929568 | |||
| c301b14cb3 | |||
| 10332fdeaf | |||
| 2107d1c928 | |||
| f7adcb8533 | |||
| ba4664e797 | |||
| b14eb1c423 | |||
| 4297d0d5b3 | |||
| 64615a1701 | |||
| 2a0e414d8f | |||
| f2c455f23b | |||
| 0a0027f245 | |||
| 4f687c155f | |||
| f1f415daa8 | |||
| 1ee767352a | |||
| 66a123c10d | |||
| de0ddfe226 | |||
| d23f7619ec | |||
| 44de6f2207 | |||
| f6f25154a1 | |||
| 880516069d | |||
| 95970183db | |||
| 3a2c952d2a | |||
| 46b94a83f1 | |||
| 40a1d1438d | |||
| 905088cae7 | |||
| 705b375dab | |||
| 0b537b07ee | |||
| dfa6198bdd | |||
| b9312d86fd | |||
| 30a1bda93d | |||
| 0e24eeeb1c | |||
| 58d4dea09a | |||
| c4da44a443 | |||
| be3f959354 | |||
| deb69ef8ea | |||
| 5ac0aaeec0 | |||
| 515cadb916 | |||
| 4b4d64c7fc | |||
| f7e1198482 | |||
| 7243ed7f15 | |||
| 7e9a78898f | |||
| 6aab8d4121 | |||
| 70594a696b | |||
| 08b4c620f2 | |||
| 7a20cf3853 | |||
| dd186c570f | |||
| d5e9d491b6 | |||
| 28dc822e4f | |||
| def1b1e710 | |||
| 2ff9e4b0a2 | |||
| 49bfc907cd | |||
| 34314bdf46 | |||
| 9d53c53c54 | |||
| 8acd585630 | |||
| 1f5907716f | |||
| 99ae0c99c6 | |||
| a56253cd3a | |||
| a2cdce66bc | |||
| 7454832663 | |||
| 1ed185606d | |||
| d9da588c7c | |||
| e206136156 | |||
| 57b11e0dae | |||
| 2fc130d709 | |||
| 1aa6b88cfa | |||
| 329d028edd | |||
| 4955f30081 | |||
| bf7ca937ff | |||
| 2193fa4251 | |||
| 9124604b89 | |||
| 239ef4a4ec | |||
| f3b72504f1 | |||
| f098f848a3 | |||
| 22d37135ae | |||
| 61c6036d41 | |||
| 95ae474cc6 | |||
| 504bbeed91 | |||
| 0e5b3450dd | |||
| 650ae0b320 | |||
| 66cfab3b57 | |||
| 0455d5bfb8 | |||
| 112449576e | |||
| 3122f4121e | |||
| 28b8894b49 | |||
| 41d5694f1f | |||
| 7befd70e21 | |||
| 0de1e1aa3e | |||
| 6528859ea9 | |||
| 704745eb7c | |||
| 2759cfee97 | |||
| 755f0aab9d | |||
| 0e163543fc | |||
| fad2534d44 | |||
| edfc64d790 | |||
| 8f40894926 | |||
| 2befcb4e74 | |||
| 2d78c32a68 | |||
| 3d0500980a | |||
| 3551ee5077 | |||
| 4008c8dfdf | |||
| 64efbc5210 | |||
| 42e9394246 | |||
| afbe8fac52 | |||
| 2c1be99bd5 | |||
| ada94e5f5d | |||
| c8675ab600 | |||
| 37367a6741 | |||
| e7b65c8e86 | |||
| 3d1095dbd8 | |||
| da4284bac0 | |||
| f3c0b68a0f | |||
| 5a3f2fe96f | |||
| aec5e84758 | |||
| e85acee3e6 | |||
| 406cf470bc | |||
| 167ee03a1f | |||
| 61c8e996d3 | |||
| ed80f06e72 | |||
| adc9e30266 | |||
| fcd4c27fb8 | |||
| c4ef6f3799 | |||
| 073b482fed | |||
| 74939c6417 | |||
| 27a62a2a02 | |||
| ffe59b2c78 | |||
| 4226da94ec | |||
| 6e634972c9 | |||
| 984b85e0e9 | |||
| 9bb38e1df3 | |||
| 4ecd8271a2 | |||
| ce7a319d2f | |||
| 1616021451 | |||
| e2a1d813a9 | |||
| e49e488b4c | |||
| 5d4547080d | |||
| cc8910cfd6 | |||
| 9fb241dca2 | |||
| e3a0879301 | |||
| 953494e9d0 | |||
| f190626c8c | |||
| 324936e0c8 | |||
| 868ec15b16 | |||
| 775c4e68fa | |||
| 34e0c4bd4a | |||
| 323130a877 | |||
| 3b2aab74ac | |||
| bc7ef0624b | |||
| 304ce8b881 | |||
| 01011a49a2 | |||
| d24ca04878 | |||
| 640d38e5a9 | |||
| ac0c074d4b | |||
| 6be5818493 | |||
| 03edd5a639 | |||
| f5eb3f1488 | |||
| 8eab9edb15 | |||
| fcc69980e4 | |||
| 053b78a3fd | |||
| 42ded966e4 | |||
| ea9119431c | |||
| add0345f10 | |||
| e5a8324a79 | |||
| cc4e933c07 | |||
| 382d279dad | |||
| b99899f128 | |||
| f5839bf39c | |||
| 914ed5b8f3 | |||
| ca0a1569f8 | |||
| 9f10fe398a | |||
| 8869278898 | |||
| 16c9e6cc1b |
@@ -6,7 +6,7 @@ body:
|
||||
- type: checkboxes
|
||||
id: check-duplicate
|
||||
attributes:
|
||||
label: I have already checked through the existing bug reports and found no duplicates
|
||||
label: I have already checked through the existing (both open AND closed) bug reports and found no duplicates
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: true
|
||||
|
||||
@@ -44,8 +44,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -121,7 +119,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-26, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
@@ -129,8 +127,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -156,7 +152,7 @@ jobs:
|
||||
on_retry_command: pnpm cache delete
|
||||
|
||||
- name: Build and Publish to R2 (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-26'
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
|
||||
@@ -19,8 +19,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -115,7 +113,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-26, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
@@ -123,8 +121,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -156,7 +152,7 @@ jobs:
|
||||
on_retry_command: pnpm cache delete
|
||||
|
||||
- name: Build and Publish releases (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-26'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
|
||||
@@ -4,6 +4,11 @@ permissions: write-all
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker image tag (e.g. 1.12.0 or latest)'
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
@@ -33,11 +38,10 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'push' }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'push' }}
|
||||
type=semver,pattern={{major}},enable=${{ github.event_name == 'push' }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Setup Docker buildx
|
||||
|
||||
@@ -16,8 +16,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [macos-26]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
@@ -16,14 +16,13 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build and Publish releases
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
os: [macos-26, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
@@ -38,8 +38,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -65,7 +63,7 @@ jobs:
|
||||
pnpm run package:linux:pr
|
||||
|
||||
- name: Build for MacOS
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
if: ${{ matrix.os == 'macos-26' }}
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
@@ -86,7 +84,7 @@ jobs:
|
||||
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
|
||||
|
||||
- name: Zip MacOS Binaries
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
if: ${{ matrix.os == 'macos-26' }}
|
||||
run: |
|
||||
zip -r dist/macos-binaries.zip dist/*.dmg
|
||||
|
||||
@@ -105,7 +103,7 @@ jobs:
|
||||
path: dist/linux-binaries.zip
|
||||
|
||||
- name: Upload MacOS Binaries
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
if: ${{ matrix.os == 'macos-26' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: macos-binaries
|
||||
|
||||
@@ -16,8 +16,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-26, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
@@ -16,8 +16,6 @@ jobs:
|
||||
|
||||
- name: Install Node and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -36,7 +34,7 @@ jobs:
|
||||
on_retry_command: pnpm cache delete
|
||||
|
||||
- name: Build and Publish releases (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-26'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: nick-invision/retry@v3.0.2
|
||||
|
||||
@@ -12,7 +12,8 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v6
|
||||
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
issue-inactive-days: 120
|
||||
@@ -29,19 +30,19 @@ jobs:
|
||||
days-before-pr-close: 30
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
|
||||
|
||||
|
||||
If this is a **bug** and you can still reproduce this error on the <code>development</code> branch, please reply with all of the information you have about it in order to keep the issue open.
|
||||
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
|
||||
|
||||
stale-pr-message: >
|
||||
This PR has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
|
||||
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
|
||||
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'keep,security'
|
||||
exempt-issue-labels: 'keep,security,enhancement'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'keep,security'
|
||||
|
||||
@@ -12,8 +12,6 @@ jobs:
|
||||
|
||||
- name: Install Node.js and PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
+1
-3
@@ -3,9 +3,7 @@ FROM node:23-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json first to cache node_modules
|
||||
COPY package.json pnpm-lock.yaml .
|
||||
|
||||
RUN npm install -g pnpm
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
Binary file not shown.
@@ -43,7 +43,13 @@ These variables override app settings **on first run** when no persisted setting
|
||||
| `general.showVisualizerInSidebar` | `true` | `FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR` | `true` / `false` — Show visualizer in sidebar. |
|
||||
| `general.sidebarCollapsedNavigation` | `true` | `FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION` | `true` / `false` — Start with collapsed sidebar nav. |
|
||||
| `general.sidebarCollapseShared` | `false` | `FS_GENERAL_SIDEBAR_COLLAPSE_SHARED` | `true` / `false` — Share sidebar collapse state. |
|
||||
| `general.sidebarPlaylistFolders` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS` | `true` / `false` — Group playlists into folders by name separator. |
|
||||
| `general.sidebarPlaylistFolderSeparator` | `/` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR` | Character or string that separates folder levels in a playlist name. Empty = use default. |
|
||||
| `general.sidebarPlaylistFolderTreeIndent` | `16` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT` | Pixels each tree level is indented (0–64). |
|
||||
| `general.sidebarPlaylistFolderTreeLineColor` | *(empty)* | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR` | CSS color for tree connecting lines. Empty = theme default. |
|
||||
| `general.sidebarPlaylistFolderView` | `tree` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW` | `single` / `tree` / `navigation` — How folders are displayed in the sidebar. |
|
||||
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
|
||||
| `general.sidebarPlaylistMode` | `expanded` | `FS_GENERAL_SIDEBAR_PLAYLIST_MODE` | `compact` / `expanded` — Sidebar playlist row layout. |
|
||||
| `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. |
|
||||
| `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. |
|
||||
| `general.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. |
|
||||
@@ -66,6 +72,9 @@ These variables override app settings **on first run** when no persisted setting
|
||||
| `playback.scrobble.scrobbleAtDuration` | `240` | `FS_PLAYBACK_SCROBBLE_AT_DURATION` | Seconds of playback before scrobble. |
|
||||
| `playback.scrobble.scrobbleAtPercentage` | `75` | `FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE` | Percentage of track before scrobble. |
|
||||
| `playback.transcode.enabled` | `false` | `FS_PLAYBACK_TRANSCODE_ENABLED` | `true` / `false` — Enable transcoding. |
|
||||
| `playback.transcode.format` | *(unset)* | `FS_PLAYBACK_TRANSCODE_FORMAT` | Transcode format string (codec/container), e.g. server-specific value. Empty = use default. |
|
||||
| `playback.transcode.bitrate` | *(unset)* | `FS_PLAYBACK_TRANSCODE_BITRATE` | Transcode bitrate (number, kbps or as defined by server). |
|
||||
| `playback.filters` | `[]` | `FS_PLAYBACK_FILTERS` | JSON array of player filters: each object needs `id`, `field`, `operator`, `value`; optional `isEnabled`. Invalid JSON or shape is ignored. |
|
||||
|
||||
---
|
||||
|
||||
@@ -105,8 +114,11 @@ These variables override app settings **on first run** when no persisted setting
|
||||
|
||||
| Setting path | Default | Env variable | Available values / Description |
|
||||
|-------------|---------|--------------|--------------------------------|
|
||||
| `autoDJ.albumStrategy` | `similar` | `FS_AUTO_DJ_ALBUM_STRATEGY` | `similar` / `library_random`. |
|
||||
| `autoDJ.enabled` | `false` | `FS_AUTO_DJ_ENABLED` | `true` / `false`. |
|
||||
| `autoDJ.itemCount` | `5` | `FS_AUTO_DJ_ITEM_COUNT` | Number of items to add. |
|
||||
| `autoDJ.mode` | `songs` | `FS_AUTO_DJ_MODE` | `songs` / `albums`. |
|
||||
| `autoDJ.songStrategy` | `similar` | `FS_AUTO_DJ_SONG_STRATEGY` | `similar` / `library_random`. |
|
||||
| `autoDJ.timing` | `1` | `FS_AUTO_DJ_TIMING` | Timing value (number). |
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
appId: org.jeffvli.feishin
|
||||
productName: Feishin
|
||||
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
|
||||
electronVersion: 39.4.0
|
||||
electronVersion: 41.7.0
|
||||
directories:
|
||||
buildResources: assets
|
||||
files:
|
||||
@@ -40,13 +40,14 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
icon: assets/icons/icon.icns
|
||||
icon: media/feishin.icon
|
||||
type: distribution
|
||||
hardenedRuntime: false
|
||||
identity: '-'
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
|
||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||
|
||||
dmg:
|
||||
@@ -62,7 +63,7 @@ linux:
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
|
||||
toolsets:
|
||||
appimage: '1.0.2'
|
||||
appimage: '1.0.3'
|
||||
|
||||
npmRebuild: false
|
||||
|
||||
@@ -70,4 +71,5 @@ publish:
|
||||
provider: s3
|
||||
bucket: feishin-nightly
|
||||
channel: alpha
|
||||
region: auto
|
||||
endpoint: https://065f090c64de2dc707dd70ac72db9669.r2.cloudflarestorage.com
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
appId: org.jeffvli.feishin
|
||||
productName: Feishin
|
||||
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
|
||||
electronVersion: 39.4.0
|
||||
electronVersion: 41.7.0
|
||||
directories:
|
||||
buildResources: assets
|
||||
files:
|
||||
@@ -40,13 +40,14 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
icon: assets/icons/icon.icns
|
||||
icon: media/feishin.icon
|
||||
type: distribution
|
||||
hardenedRuntime: false
|
||||
identity: '-'
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
|
||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||
|
||||
dmg:
|
||||
@@ -62,7 +63,7 @@ linux:
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
|
||||
toolsets:
|
||||
appimage: '1.0.2'
|
||||
appimage: '1.0.3'
|
||||
|
||||
npmRebuild: false
|
||||
publish:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
appId: org.jeffvli.feishin
|
||||
productName: Feishin
|
||||
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
|
||||
electronVersion: 39.4.0
|
||||
electronVersion: 41.7.0
|
||||
directories:
|
||||
buildResources: assets
|
||||
files:
|
||||
@@ -40,13 +40,14 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
icon: assets/icons/icon.icns
|
||||
icon: media/feishin.icon
|
||||
type: distribution
|
||||
hardenedRuntime: false
|
||||
identity: '-'
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
extendInfo:
|
||||
NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
|
||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||
|
||||
dmg:
|
||||
@@ -62,7 +63,7 @@ linux:
|
||||
artifactName: ${productName}-${os}-${arch}.${ext}
|
||||
|
||||
toolsets:
|
||||
appimage: '1.0.2'
|
||||
appimage: '1.0.3'
|
||||
|
||||
npmRebuild: false
|
||||
afterAllArtifactBuild: scripts/after-all-artifact-build.mjs
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env xdg-open
|
||||
[Desktop Entry]
|
||||
Name=Feishin
|
||||
GenericName=Music player
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><g style="display:inline" transform="translate(-53.452 -43.352)scale(1.11813)"><circle cx="256" cy="240.312" r="21.5" style="opacity:1;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.19597;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke;filter:url(#filter249)"/><path d="M220.85 277.951 183.5 315.6l36 36.1 20-19.7s5.856-6.2 16.5-6.2 16.5 6.2 16.5 6.2l20 19.7 36-36.1-37.35-37.649A51.5 51.5 0 0 1 256 291.812a51.5 51.5 0 0 1-35.15-13.86" style="opacity:1;fill:#000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter249)"/><path d="M256 145.4a25.7 25.7 0 0 0-18.229 7.551L66.97 323.47A25.42 25.42 0 0 0 59.5 341.5c0 14.083 11.417 25.5 25.5 25.5a25.42 25.42 0 0 0 18.031-7.469l103.895-103.597a51.5 51.5 0 0 1-2.426-15.621 51.5 51.5 0 0 1 51.5-51.5 51.5 51.5 0 0 1 51.5 51.5 51.5 51.5 0 0 1-2.426 15.62L408.97 359.532A25.42 25.42 0 0 0 427 367c14.083 0 25.5-11.417 25.5-25.5a25.42 25.42 0 0 0-7.469-18.031L274.23 152.95a25.7 25.7 0 0 0-18.229-7.55" style="display:inline;opacity:1;fill:#000;fill-opacity:1;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;filter:url(#filter249)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.87416,0.87416,0.87416,1.00000",
|
||||
"display-p3:0.99575,0.99575,0.99575,1.00000"
|
||||
],
|
||||
"orientation" : {
|
||||
"start" : {
|
||||
"x" : 0.5,
|
||||
"y" : 1
|
||||
},
|
||||
"stop" : {
|
||||
"x" : 0.5,
|
||||
"y" : 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "system-dark"
|
||||
}
|
||||
],
|
||||
"groups" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "normal"
|
||||
}
|
||||
],
|
||||
"blur-material-specializations" : [
|
||||
{
|
||||
"value" : 0.7
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : 0.7
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : null
|
||||
}
|
||||
],
|
||||
"hidden" : false,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "normal"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "extended-gray:0.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.78674,0.78674,0.78674,1.00000",
|
||||
"display-p3:0.87416,0.87416,0.87416,1.00000"
|
||||
],
|
||||
"orientation" : {
|
||||
"start" : {
|
||||
"x" : 0.5,
|
||||
"y" : 1
|
||||
},
|
||||
"stop" : {
|
||||
"x" : 0.5,
|
||||
"y" : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"solid" : "gray:1.00000,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass-specializations" : [
|
||||
{
|
||||
"value" : true
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : true
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : true
|
||||
}
|
||||
],
|
||||
"hidden" : false,
|
||||
"image-name" : "feishin.svg",
|
||||
"name" : "feishin",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.79,
|
||||
"translation-in-points" : [
|
||||
18,
|
||||
-2
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting-specializations" : [
|
||||
{
|
||||
"value" : "individual"
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "combined"
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 2.2,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"shadow-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"specular-specializations" : [
|
||||
{
|
||||
"value" : false
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : false
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : true
|
||||
}
|
||||
],
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.29
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"squares" : [
|
||||
"macOS"
|
||||
]
|
||||
}
|
||||
}
|
||||
+45
-49
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "1.11.0",
|
||||
"version": "1.13.0",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
@@ -69,79 +69,80 @@
|
||||
"postversion": "node ./scripts/update-app-stream.mjs"
|
||||
},
|
||||
"resolutions": {
|
||||
"react-router": "7.14.0",
|
||||
"xml2js": "0.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.5",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.2.0",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@mantine/colors-generator": "^8.3.18",
|
||||
"@mantine/core": "^8.3.18",
|
||||
"@mantine/dates": "^8.3.18",
|
||||
"@mantine/form": "^8.3.18",
|
||||
"@mantine/hooks": "^8.3.18",
|
||||
"@mantine/modals": "^8.3.18",
|
||||
"@mantine/notifications": "^8.3.18",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@tanstack/react-query": "^5.96.2",
|
||||
"@tanstack/react-query-devtools": "^5.96.2",
|
||||
"@tanstack/react-query-persist-client": "^5.96.2",
|
||||
"@mantine/colors-generator": "^9.3.0",
|
||||
"@mantine/core": "^9.3.0",
|
||||
"@mantine/dates": "^9.3.0",
|
||||
"@mantine/form": "^9.3.0",
|
||||
"@mantine/hooks": "^9.3.0",
|
||||
"@mantine/modals": "^9.3.0",
|
||||
"@mantine/notifications": "^9.3.0",
|
||||
"@radix-ui/react-context-menu": "^2.3.0",
|
||||
"@tanstack/react-query": "5.96.2",
|
||||
"@tanstack/react-query-devtools": "5.96.2",
|
||||
"@tanstack/react-query-persist-client": "5.96.2",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@wavesurfer/react": "^1.0.12",
|
||||
"@xhayper/discord-rpc": "^1.3.3",
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"audiomotion-analyzer": "^4.5.4",
|
||||
"axios": "^1.14.0",
|
||||
"axios": "^1.17.0",
|
||||
"butterchurn": "3.0.0-beta.5",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"cheerio": "^1.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"dompurify": "^3.3.3",
|
||||
"dayjs": "^1.11.21",
|
||||
"dompurify": "^3.4.11",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-log": "^5.4.4",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.8.3",
|
||||
"electron-updater": "^6.8.9",
|
||||
"fast-average-color": "9.5.0",
|
||||
"fast-xml-parser": "^5.5.10",
|
||||
"fast-xml-parser": "^5.8.0",
|
||||
"format-duration": "^3.0.2",
|
||||
"fuse.js": "^7.2.0",
|
||||
"fuse.js": "^7.4.2",
|
||||
"i18next": "^25.10.10",
|
||||
"icecast-metadata-stats": "^0.1.12",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"idb-keyval": "^6.2.5",
|
||||
"immer": "^10.2.0",
|
||||
"is-electron": "^2.2.2",
|
||||
"kuroshiro": "^1.2.0",
|
||||
"kuroshiro-analyzer-kuromoji": "^1.1.0",
|
||||
"lodash": "^4.18.1",
|
||||
"md5": "^2.3.0",
|
||||
"motion": "^12.38.0",
|
||||
"motion": "^12.40.0",
|
||||
"mpris-service": "^2.1.2",
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||
"overlayscrollbars": "^2.14.0",
|
||||
"overlayscrollbars": "^2.16.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"qs": "^6.15.0",
|
||||
"react": "^19.2.4",
|
||||
"qs": "^6.15.2",
|
||||
"react": "^19.2.7",
|
||||
"react-call": "^1.8.2",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-i18next": "^16.6.6",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-player": "^2.16.1",
|
||||
"react-router": "^7.14.0",
|
||||
"react-router": "^7.17.0",
|
||||
"react-split-pane": "^3.2.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.11",
|
||||
"react-window-v2": "npm:react-window@^2.2.7",
|
||||
"semver": "^7.7.4",
|
||||
"semver": "^7.8.2",
|
||||
"string-to-color": "^2.2.2",
|
||||
"wavesurfer.js": "^7.12.5",
|
||||
"ws": "^8.20.0",
|
||||
"wavesurfer.js": "^7.12.7",
|
||||
"ws": "^8.21.0",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.12"
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
@@ -150,8 +151,8 @@
|
||||
"@types/electron-localshortcut": "^3.1.3",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/md5": "^2.3.6",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/node": "^24.13.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
@@ -160,37 +161,32 @@
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^39.8.6",
|
||||
"electron-builder": "^26.8.2",
|
||||
"electron": "^41.7.1",
|
||||
"electron-builder": "^26.15.5",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron-vite": "^4.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-perfectionist": "^4.15.1",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"i18next-parser": "^9.4.0",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-packagejson": "^2.5.22",
|
||||
"stylelint": "^16.26.1",
|
||||
"stylelint-config-css-modules": "^4.6.0",
|
||||
"stylelint-config-recess-order": "^7.7.0",
|
||||
"stylelint-config-standard": "^39.0.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite": "^7.3.5",
|
||||
"vite-plugin-conditional-import": "^0.1.7",
|
||||
"vite-plugin-dynamic-import": "^1.6.0",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
"vite-plugin-pwa": "^1.3.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.5.2",
|
||||
"productName": "feishin"
|
||||
}
|
||||
|
||||
Generated
+2297
-2789
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
allowBuilds:
|
||||
abstract-socket: true
|
||||
electron: true
|
||||
electron-winstaller: true
|
||||
esbuild: true
|
||||
minimumReleaseAge: 1440
|
||||
overrides:
|
||||
'xml2js': '0.5.0'
|
||||
'react-router': '7.14.0'
|
||||
@@ -38,7 +38,13 @@ window.FS_GENERAL_SHOW_RATINGS = "${FS_GENERAL_SHOW_RATINGS}";
|
||||
window.FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR = "${FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR}";
|
||||
window.FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION = "${FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION}";
|
||||
window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARED}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_MODE = "${FS_GENERAL_SIDEBAR_PLAYLIST_MODE}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}";
|
||||
window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}";
|
||||
window.FS_GENERAL_SIDE_QUEUE_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}";
|
||||
@@ -58,6 +64,9 @@ window.FS_PLAYBACK_SCROBBLE_NOTIFY = "${FS_PLAYBACK_SCROBBLE_NOTIFY}";
|
||||
window.FS_PLAYBACK_SCROBBLE_AT_DURATION = "${FS_PLAYBACK_SCROBBLE_AT_DURATION}";
|
||||
window.FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE = "${FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE}";
|
||||
window.FS_PLAYBACK_TRANSCODE_ENABLED = "${FS_PLAYBACK_TRANSCODE_ENABLED}";
|
||||
window.FS_PLAYBACK_TRANSCODE_FORMAT = "${FS_PLAYBACK_TRANSCODE_FORMAT}";
|
||||
window.FS_PLAYBACK_TRANSCODE_BITRATE = "${FS_PLAYBACK_TRANSCODE_BITRATE}";
|
||||
window.FS_PLAYBACK_FILTERS = "${FS_PLAYBACK_FILTERS}";
|
||||
|
||||
window.FS_DISCORD_ENABLED = "${FS_DISCORD_ENABLED}";
|
||||
window.FS_DISCORD_CLIENT_ID = "${FS_DISCORD_CLIENT_ID}";
|
||||
@@ -79,8 +88,11 @@ window.FS_LYRICS_TRANSLATION_API_KEY = "${FS_LYRICS_TRANSLATION_API_KEY}";
|
||||
window.FS_LYRICS_TRANSLATION_TARGET_LANGUAGE = "${FS_LYRICS_TRANSLATION_TARGET_LANGUAGE}";
|
||||
window.FS_LYRICS_ALIGNMENT = "${FS_LYRICS_ALIGNMENT}";
|
||||
|
||||
window.FS_AUTO_DJ_ALBUM_STRATEGY = "${FS_AUTO_DJ_ALBUM_STRATEGY}";
|
||||
window.FS_AUTO_DJ_ENABLED = "${FS_AUTO_DJ_ENABLED}";
|
||||
window.FS_AUTO_DJ_ITEM_COUNT = "${FS_AUTO_DJ_ITEM_COUNT}";
|
||||
window.FS_AUTO_DJ_MODE = "${FS_AUTO_DJ_MODE}";
|
||||
window.FS_AUTO_DJ_SONG_STRATEGY = "${FS_AUTO_DJ_SONG_STRATEGY}";
|
||||
window.FS_AUTO_DJ_TIMING = "${FS_AUTO_DJ_TIMING}";
|
||||
|
||||
window.FS_CSS_CONTENT = "${FS_CSS_CONTENT}";
|
||||
|
||||
+22
-12
@@ -1,4 +1,4 @@
|
||||
import { PostProcessorModule, TOptions } from 'i18next';
|
||||
import { PostProcessorModule } from 'i18next';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
@@ -8,6 +8,7 @@ import cs from './locales/cs.json';
|
||||
import de from './locales/de.json';
|
||||
import en from './locales/en.json';
|
||||
import es from './locales/es.json';
|
||||
import et from './locales/et.json';
|
||||
import eu from './locales/eu.json';
|
||||
import fa from './locales/fa.json';
|
||||
import fi from './locales/fi.json';
|
||||
@@ -27,6 +28,8 @@ import sl from './locales/sl.json';
|
||||
import sr from './locales/sr.json';
|
||||
import sv from './locales/sv.json';
|
||||
import ta from './locales/ta.json';
|
||||
import th from './locales/th.json';
|
||||
import tl from './locales/tl.json';
|
||||
import tr from './locales/tr.json';
|
||||
import zhHans from './locales/zh-Hans.json';
|
||||
import zhHant from './locales/zh-Hant.json';
|
||||
@@ -38,6 +41,7 @@ const resources = {
|
||||
de: { translation: de },
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
et: { translation: et },
|
||||
eu: { translation: eu },
|
||||
fa: { translation: fa },
|
||||
fi: { translation: fi },
|
||||
@@ -57,6 +61,8 @@ const resources = {
|
||||
sr: { translation: sr },
|
||||
sv: { translation: sv },
|
||||
ta: { translation: ta },
|
||||
th: { translation: th },
|
||||
tl: { translation: tl },
|
||||
tr: { translation: tr },
|
||||
'zh-Hans': { translation: zhHans },
|
||||
'zh-Hant': { translation: zhHant },
|
||||
@@ -87,6 +93,10 @@ export const languages = [
|
||||
label: 'Español',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
label: 'Eesti',
|
||||
value: 'et',
|
||||
},
|
||||
{
|
||||
label: 'Basque',
|
||||
value: 'eu',
|
||||
@@ -163,6 +173,14 @@ export const languages = [
|
||||
label: 'Tamil',
|
||||
value: 'ta',
|
||||
},
|
||||
{
|
||||
label: 'Thai',
|
||||
value: 'th',
|
||||
},
|
||||
{
|
||||
label: 'Tagalog',
|
||||
value: 'tl',
|
||||
},
|
||||
{
|
||||
label: 'Türkçe',
|
||||
value: 'tr',
|
||||
@@ -203,25 +221,17 @@ const titleCasePostProcessor: PostProcessorModule = {
|
||||
type: 'postProcessor',
|
||||
};
|
||||
|
||||
const ignoreSentenceCaseLanguages = ['de'];
|
||||
// const ignoreSentenceCaseLanguages = ['de'];
|
||||
|
||||
const sentenceCasePostProcessor: PostProcessorModule = {
|
||||
name: 'sentenceCase',
|
||||
process: (
|
||||
value: string,
|
||||
_key: string,
|
||||
_options: TOptions<Record<string, string>>,
|
||||
translator: any,
|
||||
) => {
|
||||
process: (value: string) => {
|
||||
const sentences = value.split('. ');
|
||||
|
||||
return sentences
|
||||
.map((sentence) => {
|
||||
return (
|
||||
sentence.charAt(0).toLocaleUpperCase() +
|
||||
(!ignoreSentenceCaseLanguages.includes(translator.language)
|
||||
? sentence.slice(1).toLocaleLowerCase()
|
||||
: sentence.slice(1))
|
||||
sentence.charAt(0).toLocaleUpperCase() + sentence.slice(1).toLocaleLowerCase()
|
||||
);
|
||||
})
|
||||
.join('. ');
|
||||
|
||||
+349
-15
@@ -2,32 +2,48 @@
|
||||
"action": {
|
||||
"addToFavorites": "إضافة الى $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "إضافة الى $t(entity.playlist, {\"count\": 1})",
|
||||
"clearQueue": "مسح قائمة الإنتظار",
|
||||
"clearQueue": "مسح قائمة التشغيل",
|
||||
"createPlaylist": "إنشاء $t(entity.playlist, {\"count\": 1})",
|
||||
"deletePlaylist": "حذف $t(entity.playlist, {\"count\": 1})",
|
||||
"deselectAll": "إلغاء تحديد الكل",
|
||||
"editPlaylist": "تعديل $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "اذهب الى صفحة",
|
||||
"moveToNext": "الذهاب الى التالي",
|
||||
"moveToBottom": "الذهاب الى الأسفل",
|
||||
"moveToTop": "الذهاب الى الأعلى",
|
||||
"goToPage": "اذهب الى الصفحة",
|
||||
"moveToNext": "نقل إلى التالي",
|
||||
"moveToBottom": "نقل إلى الأسفل",
|
||||
"moveToTop": "نقل إلى الأعلى",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "حذف من $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "حذف من $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "حذف من قائمة الإنتظار",
|
||||
"removeFromQueue": "حذف من قائمة التشغيل",
|
||||
"setRating": "تحديد التقييم",
|
||||
"toggleSmartPlaylistEditor": "تشغيل / إطفاء وضع التعديل لـ $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "إظهار $t(entity.playlist, {\"count\": 2})",
|
||||
"toggleSmartPlaylistEditor": "إظهار / إخفاء وضع التعديل لـ $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "عرض $t(entity.playlist, {\"count\": 2})",
|
||||
"openIn": {
|
||||
"lastfm": "فتح في Last.fm",
|
||||
"musicbrainz": "فتح في MusicBrainz"
|
||||
"musicbrainz": "فتح في MusicBrainz",
|
||||
"listenbrainz": "فتح في ListenBrainz",
|
||||
"qobuz": "فتح في Qobuz",
|
||||
"spotify": "فتح في Spotify"
|
||||
},
|
||||
"addOrRemoveFromSelection": "إضافة أو إزالة من الإختيارات",
|
||||
"selectRangeOfItems": "اختر مجموعة من العناصر",
|
||||
"goToCurrent": "الانتقال إلى العنصر الحالي",
|
||||
"createRadioStation": "يخلق $t(entity.radioStation, {\"count\": 1})",
|
||||
"createRadioStation": "إنشاء $t(entity.radioStation, {\"count\": 1})",
|
||||
"deleteRadioStation": "يمسح $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "تحديد الكل"
|
||||
"selectAll": "تحديد الكل",
|
||||
"shuffle": "لخبط",
|
||||
"shuffleAll": "لخبط الكل",
|
||||
"shuffleSelected": "لخبط المحدد",
|
||||
"collapseAllFolders": "اطو جميع المجلدات",
|
||||
"expandAllFolders": "بسط الملفات",
|
||||
"downloadStarted": "بدأ تحميل {{count}} عنصر",
|
||||
"moveUp": "نقل إلى فوق",
|
||||
"moveDown": "نقل إلى تحت",
|
||||
"holdToMoveToTop": "اضغط مطولاً للنقل إلى الأعلى",
|
||||
"holdToMoveToBottom": "اضغط مطولاً للنقل إلى الأسفل",
|
||||
"moveItems": "نقل العناصر",
|
||||
"viewMore": "عرض المزيد",
|
||||
"openApplicationDirectory": "فتح مجلد التطبيق"
|
||||
},
|
||||
"common": {
|
||||
"action_zero": "عملية",
|
||||
@@ -39,13 +55,13 @@
|
||||
"add": "إضافة",
|
||||
"additionalParticipants": "مشاركين إضافيين",
|
||||
"newVersion": "تم تثبيت تحديث جديد {{version}}",
|
||||
"viewReleaseNotes": "عرض معلومات الإصدار",
|
||||
"viewReleaseNotes": "عرض ملاحظات الإصدار",
|
||||
"albumGain": "مستوى صوت الألبوم",
|
||||
"albumPeak": "اعلى مستوى للألبوم",
|
||||
"areYouSure": "هل أنت متأكد؟",
|
||||
"ascending": "تصاعدي",
|
||||
"backward": "خلف",
|
||||
"biography": "سيرة",
|
||||
"biography": "السيرة",
|
||||
"bitDepth": "عمق البت",
|
||||
"bitrate": "معدل البت (البت ريت)",
|
||||
"bpm": "نبضة في الدقيقة",
|
||||
@@ -141,7 +157,35 @@
|
||||
"unknown": "غير معروف",
|
||||
"version": "الإصدار",
|
||||
"year": "السنة",
|
||||
"yes": "نعم"
|
||||
"yes": "نعم",
|
||||
"explicitStatus": "حالة المحتوى الصريح",
|
||||
"countSelected": "{{count}} عنصر محدد",
|
||||
"back": "للخلف",
|
||||
"doNotShowAgain": "لا تظهر هذا مجدداً",
|
||||
"view": "عرض",
|
||||
"example": "مثال",
|
||||
"externalLinks": "روابط الخارجية",
|
||||
"openFolder": "فتح المجلد",
|
||||
"faster": "أسرع",
|
||||
"filter_single": "فردي",
|
||||
"filter_multiple": "متعدد",
|
||||
"grouping": "مجموعات",
|
||||
"mood": "مزاج",
|
||||
"numberOfResults": "{{numberOfResults}} نتيجة",
|
||||
"noFilters": "لا توجد فلاتر معينة",
|
||||
"private": "خاص",
|
||||
"public": "عام",
|
||||
"retry": "إعادة المحاولة",
|
||||
"recordLabel": "شركة التسجيل",
|
||||
"releaseType": "نوع الإصدار",
|
||||
"rename": "إعادة تسمية",
|
||||
"slower": "أبطأ",
|
||||
"sort": "فرز",
|
||||
"explicit": "صريح",
|
||||
"clean": "نظيف",
|
||||
"gridRows": "صفوف الشبكة",
|
||||
"tableColumns": "أعمدة الجدول",
|
||||
"newVersionAvailable": "هناك نسخة جديدة متاحة"
|
||||
},
|
||||
"entity": {
|
||||
"album_zero": "الالبوم",
|
||||
@@ -155,6 +199,296 @@
|
||||
"albumArtist_two": "فنان الالبومين",
|
||||
"albumArtist_few": "فنان الالبومات",
|
||||
"albumArtist_many": "فنان الالبومات",
|
||||
"albumArtist_other": "فنان الالبومات"
|
||||
"albumArtist_other": "فنان الالبومات",
|
||||
"albumArtistCount_zero": "{{count}} فنان الالبوم",
|
||||
"albumArtistCount_one": "{{count}} فنان الالبوم",
|
||||
"albumArtistCount_two": "{{count}} فنان الالبومين",
|
||||
"albumArtistCount_few": "{{count}} فنان الالبومات",
|
||||
"albumArtistCount_many": "{{count}} فنان الالبومات",
|
||||
"albumArtistCount_other": "{{count}} فنان الالبومات",
|
||||
"albumWithCount_zero": "{{count}} البوم",
|
||||
"albumWithCount_one": "{{count}} البوم",
|
||||
"albumWithCount_two": "{{count}} البومين",
|
||||
"albumWithCount_few": "{{count}} البومات",
|
||||
"albumWithCount_many": "{{count}} البومات",
|
||||
"albumWithCount_other": "{{count}} البومات",
|
||||
"radioStation_zero": "محطة راديو",
|
||||
"radioStation_one": "محطة راديو",
|
||||
"radioStation_two": "محطتان راديو",
|
||||
"radioStation_few": "محطات راديو",
|
||||
"radioStation_many": "محطات راديو",
|
||||
"radioStation_other": "محطات راديو",
|
||||
"radioStationWithCount_zero": "{{count}} محطة راديو",
|
||||
"radioStationWithCount_one": "{{count}} محطة راديو",
|
||||
"radioStationWithCount_two": "{{count}} محطتان راديو",
|
||||
"radioStationWithCount_few": "{{count}} محطات راديو",
|
||||
"radioStationWithCount_many": "{{count}} محطات راديو",
|
||||
"radioStationWithCount_other": "{{count}} محطات راديو",
|
||||
"artist_zero": "فنان",
|
||||
"artist_one": "فنان",
|
||||
"artist_two": "فنانان",
|
||||
"artist_few": "فنانين",
|
||||
"artist_many": "فنانين",
|
||||
"artist_other": "فنانين",
|
||||
"artistWithCount_zero": "{{count}} فنان",
|
||||
"artistWithCount_one": "{{count}} فنان",
|
||||
"artistWithCount_two": "{{count}} فنانان",
|
||||
"artistWithCount_few": "{{count}} فنانين",
|
||||
"artistWithCount_many": "{{count}} فنانين",
|
||||
"artistWithCount_other": "{{count}} فنانين",
|
||||
"favorite_zero": "مفضلة",
|
||||
"favorite_one": "مفضلة",
|
||||
"favorite_two": "مفضلتان",
|
||||
"favorite_few": "مفضلات",
|
||||
"favorite_many": "مفضلات",
|
||||
"favorite_other": "مفضلات",
|
||||
"folder_zero": "مجلد",
|
||||
"folder_one": "مجلد",
|
||||
"folder_two": "مجلدان",
|
||||
"folder_few": "مجلدات",
|
||||
"folder_many": "مجلدات",
|
||||
"folder_other": "مجلدات",
|
||||
"folderWithCount_zero": "{{count}} مجلد",
|
||||
"folderWithCount_one": "{{count}} مجلد",
|
||||
"folderWithCount_two": "{{count}} مجلدان",
|
||||
"folderWithCount_few": "{{count}} مجلدات",
|
||||
"folderWithCount_many": "{{count}} مجلدات",
|
||||
"folderWithCount_other": "{{count}} مجلدات",
|
||||
"genre_zero": "نوع",
|
||||
"genre_one": "نوع",
|
||||
"genre_two": "نوعان",
|
||||
"genre_few": "أنواع",
|
||||
"genre_many": "أنواع",
|
||||
"genre_other": "أنواع",
|
||||
"genreWithCount_zero": "{{count}} نوع",
|
||||
"genreWithCount_one": "{{count}} نوع",
|
||||
"genreWithCount_two": "{{count}} نوعان",
|
||||
"genreWithCount_few": "{{count}} أنواع",
|
||||
"genreWithCount_many": "{{count}} أنواع",
|
||||
"genreWithCount_other": "{{count}} أنواع",
|
||||
"playlist_zero": "قائمة تشغيل",
|
||||
"playlist_one": "قائمة تشغيل",
|
||||
"playlist_two": "قائمتان تشغيل",
|
||||
"playlist_few": "قوائم تشغيل",
|
||||
"playlist_many": "قوائم تشغيل",
|
||||
"playlist_other": "قوائم تشغيل",
|
||||
"play_zero": "{{count}} قائمة تشغيل",
|
||||
"play_one": "{{count}} قائمة تشغيل",
|
||||
"play_two": "{{count}} قائمتان تشغيل",
|
||||
"play_few": "{{count}} قوائم تشغيل",
|
||||
"play_many": "{{count}} قوائم تشغيل",
|
||||
"play_other": "{{count}} قوائم تشغيل",
|
||||
"playlistWithCount_zero": "{{count}} قائمة تشغيل",
|
||||
"playlistWithCount_one": "{{count}} قائمة تشغيل",
|
||||
"playlistWithCount_two": "{{count}} قائمتان تشغيل",
|
||||
"playlistWithCount_few": "{{count}} قوائم تشغيل",
|
||||
"playlistWithCount_many": "{{count}} قوائم تشغيل",
|
||||
"playlistWithCount_other": "{{count}} قوائم تشغيل",
|
||||
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) قائمة تشغيل ذكية",
|
||||
"track_zero": "مقطع",
|
||||
"track_one": "مقطع",
|
||||
"track_two": "مقطعان",
|
||||
"track_few": "مقاطع",
|
||||
"track_many": "مقاطع",
|
||||
"track_other": "مقاطع",
|
||||
"song_zero": "أغنية",
|
||||
"song_one": "أغنية",
|
||||
"song_two": "أغنيتان",
|
||||
"song_few": "أغاني",
|
||||
"song_many": "أغاني",
|
||||
"song_other": "أغاني",
|
||||
"trackWithCount_zero": "{{count}} مقطع",
|
||||
"trackWithCount_one": "{{count}} مقطع",
|
||||
"trackWithCount_two": "{{count}} مقطعان",
|
||||
"trackWithCount_few": "{{count}} مقاطع",
|
||||
"trackWithCount_many": "{{count}} مقاطع",
|
||||
"trackWithCount_other": "{{count}} مقاطع"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "تعذّر توجيه الطلب",
|
||||
"audioDeviceFetchError": "حصل خطأ أثناء محاولة الحصول على أجهزة الصوت",
|
||||
"authenticationFailed": "فشلت المصادقة",
|
||||
"badAlbum": "أنت ترى هذة الصفحة لأن هذه الأغنية ليست جزءاً من ألبوم. على الأرجح تظهر لك هذه المشكلة إذا كان لديك أغنية في المستوى الأعلى من مجلد الموسيقى. يقوم Jellyfin بتجميع الأغاني فقط إذا كانت داخل مجلد",
|
||||
"credentialsRequired": "يتطلب بيانات اعتماد",
|
||||
"genericError": "حدث خطأ",
|
||||
"loginRateError": "تجاوزت الحد لمحاولات الدخول. حاول مجدداً بعد بضع ثوان",
|
||||
"mpvRequired": "يتطلب MPV",
|
||||
"multipleServerSaveQueueError": "قائمة التشغيل تحتوي على أغنية أو أكثر من خادم مختلف. هذا غير مدعوم",
|
||||
"networkError": "حصل خطأ في الشبكة",
|
||||
"noNetwork": "الخادم غير متوفر",
|
||||
"noNetworkDescription": "تعذر الإتصال بالخادم",
|
||||
"notificationDenied": "تم رفض أذن الإشعارات. هذا الإعداد لن يكون له أي تأثير",
|
||||
"openError": "تعذر فتح الملف",
|
||||
"playbackError": "حدث خطأ أثناء محاولة تشغيل الوسائط",
|
||||
"playbackPausedDueToError": "تم ايقاف التشغيل بسبب خطأ",
|
||||
"remoteDisableError": "حدث خطأ أثناء محاولة $t(common.disable) الخادم البعيد",
|
||||
"remoteEnableError": "حدث خطأ أثناء محاولة $t(common.enable) الخادم البعيد",
|
||||
"remotePortError": "حدث خطأ أثناء محاولة تعيين الخادم البعيد",
|
||||
"remotePortWarning": "أعد تشغيل الخادم لتطبيق المنفذ الجديد",
|
||||
"saveQueueFailed": "فشل حفظ قائمة التشغيل",
|
||||
"serverLockSingleServer": "فقط خادم واحد متاح إذا الخادم مقفل",
|
||||
"serverNotSelectedError": "لم يتم اختيار أي خادم",
|
||||
"serverRequired": "يتطلب خادم",
|
||||
"sessionExpiredError": "انتهت صلاحية جلستك",
|
||||
"systemFontError": "حدث خطأ أثناء محاولة الحصول على خطوط النظام",
|
||||
"settingsSyncError": "تم اكتشاف تعارضات بين إعدادات العارض والعملية الرئيسية. أعد تشغيل التطبيق لتطبيق التغييرات",
|
||||
"invalidJson": "JSON غير صالح",
|
||||
"invalidServer": "خادم غير صالح",
|
||||
"localFontAccessDenied": "تم رفض الوصول إلى الخطوط المحلية"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"matchAnd": "و",
|
||||
"matchOr": "أو",
|
||||
"biography": "السيرة",
|
||||
"bitrate": "معدل البت (البت ريت)",
|
||||
"bpm": "نبضة في الدقيقة",
|
||||
"comment": "تعليق",
|
||||
"communityRating": "تقييم المجتمع",
|
||||
"criticRating": "تقييم الناقد",
|
||||
"dateAdded": "تاريخ الإضافة",
|
||||
"disc": "قرص",
|
||||
"duration": "المدة",
|
||||
"favorited": "مفضل",
|
||||
"fromYear": "من سنة",
|
||||
"id": "معرف",
|
||||
"isFavorited": "مفضل",
|
||||
"isPublic": "عام",
|
||||
"isRated": "مقيم",
|
||||
"isRecentlyPlayed": "تم التشغيل حديثاً",
|
||||
"lastPlayed": "أخر تشغيل",
|
||||
"mostPlayed": "أكثر تشغيل",
|
||||
"name": "الأسم",
|
||||
"note": "الملاحظة",
|
||||
"path": "المسار",
|
||||
"playCount": "عدد التشغيلات",
|
||||
"random": "عشوائي",
|
||||
"rating": "التقييم",
|
||||
"recentlyAdded": "مضاف حديثاً",
|
||||
"recentlyPlayed": "تم التشغيل حديثاً",
|
||||
"recentlyUpdated": "محدث حديثاً",
|
||||
"releaseDate": "تاريخ الإصدار",
|
||||
"releaseYear": "سنة الإصدار",
|
||||
"search": "بحث",
|
||||
"songCount": "عدد الأغاني",
|
||||
"sortName": "أسم الفرز",
|
||||
"title": "العنوان",
|
||||
"toYear": "إلى سنة",
|
||||
"trackNumber": "مقطع",
|
||||
"isCompilation": "تجميعة"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "د",
|
||||
"secondShort": "ث",
|
||||
"hourShort": "س",
|
||||
"dayShort": "ي"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "بعد",
|
||||
"afterDate": "بعد (تاريخ)",
|
||||
"before": "قبل",
|
||||
"beforeDate": "قبل (تاريخ)",
|
||||
"contains": "يحتوي على",
|
||||
"endsWith": "ينتهي بـ",
|
||||
"inPlaylist": "في",
|
||||
"inTheLast": "في أخِر",
|
||||
"inTheRange": "في مدى",
|
||||
"inTheRangeDate": "في مدى (تاريخ)",
|
||||
"is": "في",
|
||||
"isNot": "ليس في",
|
||||
"isGreaterThan": "أكبر من",
|
||||
"isLessThan": "أقل من",
|
||||
"matchesRegex": "يطابق التعبير النمطي",
|
||||
"notContains": "لا يحتوي على",
|
||||
"notInPlaylist": "ليس في",
|
||||
"notInTheLast": "ليس في أخِر",
|
||||
"startsWith": "يبدأ بـ"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
"error_savePassword": "حدث خطأ أثناء محاولة حفظ كلمة السر",
|
||||
"input_legacyAuthentication": "تفعيل المصادقة القديمة",
|
||||
"input_name": "أسم الخادم",
|
||||
"input_password": "كلمة السر",
|
||||
"input_preferRemoteUrl": "تفضيل رابط عام",
|
||||
"input_remoteUrl": "رابط عام",
|
||||
"input_savePassword": "حفظ كلمة السر",
|
||||
"input_url": "الرابط",
|
||||
"input_username": "أسم المستخدم",
|
||||
"success": "تمت إضافة الخادم بنجاح",
|
||||
"title": "إضافة خادم",
|
||||
"input_preferInstantMix": "تفضيل الميكس الفوري",
|
||||
"input_preferInstantMixDescription": "استخدم الميكس الفوري فقط للحصول على أغاني مشابهة. مفيد إذا كان لديك إضافات تعدّل هذا السلوك",
|
||||
"input_remoteUrlPlaceholder": "اختياري: عنوان URL عام للميزات الخارجية"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "أضف العناصر إلى قائمة التشغيل",
|
||||
"description": "سيقوم هذا الإجراء بإضافة جميع العناصر في العرض المفلتر الحالي"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"input_skipDuplicates": "تخطي العناصر المكررة",
|
||||
"title": "أضف إلى $t(entity.playlist, {\"count\": 1})",
|
||||
"create": "إنشاء $t(entity.playlist, {\"count\": 1}) {{playlist}}"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_public": "عام"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"input_homepageUrl": "رابط الرئيسية",
|
||||
"input_name": "الأسم",
|
||||
"input_streamUrl": "رابط البث",
|
||||
"success": "تم إنشاء محطة راديو جديدة بنجاح",
|
||||
"title": "إنشاء محطة راديو"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "تم تحديث محطة الراديو بنجاح"
|
||||
},
|
||||
"deletePlaylist": {
|
||||
"input_confirm": "أكتب أسم $t(entity.playlist, {\"count\": 1}) للتأكيد",
|
||||
"success": "تم حذف $t(entity.playlist, {\"count\": 1}) بنجاح",
|
||||
"title": "حذف $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"success": "تم تحديث $t(entity.playlist, {\"count\": 1}) بنجاح",
|
||||
"title": "تعديل $t(entity.playlist, {\"count\": 1})",
|
||||
"publicJellyfinNote": "لسبب ما، لا يكشف Jellyfin عما إذا كانت قائمة التشغيل عامة أم لا. إذا كنت ترغب في إبقائها عامة، يرجى التأكد من تحديد الخيار التالي"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "تصدير الكلمات",
|
||||
"input_synced": "تصدير الكلمات المتزامنة"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"title": "البحث بالكلمات"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "تطابق الجميع",
|
||||
"input_optionMatchAny": "تطابق أي",
|
||||
"title": "محرر الاستعلامات",
|
||||
"addRuleGroup": "إضافة مجموعة قواعد",
|
||||
"removeRuleGroup": "إزالة مجموعة قواعد",
|
||||
"resetToDefault": "استعادة الإعدادات الافتراضية"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "السماح بالتحميل",
|
||||
"description": "الوصف"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "تشغيل عشوائي",
|
||||
"input_kind_albums": "ألبومات",
|
||||
"input_kind_songs": "أغاني",
|
||||
"input_kind": "إختيارات عشوائية",
|
||||
"input_minYear": "من سنة",
|
||||
"input_maxYear": "إلى سنة"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "تم تحديث الخادم بنجاح",
|
||||
"title": "تحديث الخادم"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"albumArtistDetail": {
|
||||
"favoriteSongs": "الأغاني المفضلة"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1057
-947
File diff suppressed because it is too large
Load Diff
+1028
-945
File diff suppressed because it is too large
Load Diff
+870
-871
File diff suppressed because it is too large
Load Diff
+431
-420
File diff suppressed because it is too large
Load Diff
+1001
-918
File diff suppressed because it is too large
Load Diff
+627
-544
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+784
-744
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
"unfavorite": "حذف از موردعلاقهها",
|
||||
"shuffle_off": "پخش تصادفی غیر فعال",
|
||||
"skip_forward": "برو جلو",
|
||||
"queue_moveToTop": "جابجا کردن انتخاب شده به پایین",
|
||||
"queue_moveToTop": "جابجا کردن انتخاب شده به بالا",
|
||||
"queue_clear": "خالی کردن صف",
|
||||
"queue_remove": "حذف انتخاب شده",
|
||||
"addLast": "افزودن به پایان",
|
||||
@@ -24,7 +24,7 @@
|
||||
"mute": "بیصدا کردن",
|
||||
"playbackFetchCancel": "دارد طول میکشد... برای لفو کردن اعلان را ببندید",
|
||||
"playbackFetchInProgress": "بارگذاری قطعهها…",
|
||||
"queue_moveToBottom": "جابجا کردن انتخاب شده به بالا",
|
||||
"queue_moveToBottom": "جابجا کردن انتخاب شده به پایین",
|
||||
"addNext": "افزودن به پسین",
|
||||
"favorite": "مورد علاقه",
|
||||
"playSimilarSongs": "پخش آهنگهای همگون",
|
||||
@@ -70,7 +70,7 @@
|
||||
"hotkey_rate1": "امتیاز ۱ ستاره",
|
||||
"hotkey_skipForward": "برو جلو",
|
||||
"disableLibraryUpdateOnStartup": "غیرفعال کردن بررسی آخرین نسخه در آغاز به کار برنامه",
|
||||
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
||||
"discordApplicationId_description": "the application ID for {{discord}} Rich Presence (defaults to {{defaultId}})",
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"hotkey_playbackPlay": "پخش",
|
||||
"hotkey_volumeDown": "کم کردن صدا",
|
||||
@@ -109,7 +109,7 @@
|
||||
"customFontPath": "مسیر قلم سفارشی",
|
||||
"audioPlayer": "پخشکنندهٔ صدا",
|
||||
"hotkey_rate0": "حذف امتیاز",
|
||||
"discordApplicationId": "{{discord}} application id",
|
||||
"discordApplicationId": "{{discord}} application ID",
|
||||
"hotkey_volumeMute": "بستن صدا",
|
||||
"showSkipButton": "نمایش دکمهٔ رد کردن",
|
||||
"customFontPath_description": "مسیر قلم سفارشی را برای استفاده در اپلیکیشن مشخص کنید",
|
||||
@@ -132,7 +132,7 @@
|
||||
"buttonSize": "اندازهی دکمهی پخش نوار",
|
||||
"contextMenu": "پیکربندی فهرست زمینه (کلیک راست)",
|
||||
"buttonSize_description": "اندازهی دکمههای پخش نوار",
|
||||
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال میکند. در این حالت، سامانه معمولاً قفل است و فقط mpv میتواند خروجی صدا دهد",
|
||||
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال میکند. در این حالت، سامانه معمولاً قفل است و فقط MPV میتواند خروجی صدا دهد",
|
||||
"clearQueryCache_description": "یک 'پاکسازی نرم' از فیشین. این فهرستهای پخش و فرادادهی قطعهها را تازه میکند و متن شعرهای ذخیره شده را بازنشانی میکند. پیکربندیها، اعتبارنامههای سرویسدهنده و نگارههای کَش شده حفظ میشوند",
|
||||
"clearCache_description": "یک 'پاکسازی سخت' فیشین. افزون بر پاکسازی کَش فیشین، کَش مرورگر هم تهی میشود (نگارههای ذخیره شده و باقی داراییها). اعتبارنامهها و پیکربندیها حفظ میشوند",
|
||||
"contextMenu_description": "به شما اجازه میدهد که آیتمهای نمایش داده شده در فهرستی که وقتی روی یک آیتم کلیک راست میکنید پدیدار میشود، را پنهان کنید. آیتمهایی که منتخب نیستند پنهان میشوند",
|
||||
@@ -176,7 +176,7 @@
|
||||
"backward": "به عقب",
|
||||
"increase": "افزایش",
|
||||
"rating": "امتیاز",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"refresh": "تازهسازی",
|
||||
"unknown": "ناشناخته",
|
||||
"areYouSure": "مطمئنید؟",
|
||||
@@ -313,9 +313,9 @@
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"isRecentlyPlayed": "به تازگی پخش شده است",
|
||||
"isFavorited": "موردعلاقه است",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"releaseYear": "سال انتشار",
|
||||
"id": "id",
|
||||
"id": "ID",
|
||||
"disc": "دیسک",
|
||||
"biography": "زندگینامه",
|
||||
"songCount": "تعداد ترانه",
|
||||
|
||||
+752
-753
File diff suppressed because it is too large
Load Diff
+1029
-989
File diff suppressed because it is too large
Load Diff
+727
-728
File diff suppressed because it is too large
Load Diff
+940
-877
File diff suppressed because it is too large
Load Diff
+744
-745
File diff suppressed because it is too large
Load Diff
+71
-43
@@ -22,8 +22,8 @@
|
||||
"queue_clear": "キューをクリア",
|
||||
"muted": "ミュート中",
|
||||
"unfavorite": "お気に入り解除",
|
||||
"queue_moveToTop": "選択項目を一番下に移動",
|
||||
"queue_moveToBottom": "選択項目を先頭に移動",
|
||||
"queue_moveToTop": "選択項目を先頭に移動",
|
||||
"queue_moveToBottom": "選択項目を一番下に移動",
|
||||
"shuffle_off": "シャッフル無効",
|
||||
"addLast": "最後",
|
||||
"mute": "ミュート",
|
||||
@@ -48,7 +48,9 @@
|
||||
"holdToShuffle": "長押しでシャッフル",
|
||||
"albumRadio": "アルバム・ラジオ",
|
||||
"artistRadio": "アーティストラジオ",
|
||||
"trackRadio": "ラジオを追跡する"
|
||||
"trackRadio": "ラジオを追跡する",
|
||||
"scrobbleForceSubmit": "強制 Scrobble",
|
||||
"sleepTimer_endOfAlbum": "現在のアルバムの終了"
|
||||
},
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "オーディオプレーヤーが使用するクロスフェードのスタイルを選択します",
|
||||
@@ -85,7 +87,7 @@
|
||||
"hotkey_zoomIn": "拡大",
|
||||
"scrobble_description": "再生した音楽をメディアサーバーから Scrobble します",
|
||||
"hotkey_browserForward": "ブラウザ 進む",
|
||||
"audioExclusiveMode_description": "排他出力モードを有効にします。このモードでは、システムの他の出力がロックされ、MPV のみがオーディオを出力できるようになります",
|
||||
"audioExclusiveMode_description": "排他出力モードを有効にします。このモードでは、システムの他の出力がロックされ、MPV のみがオーディオを出力できるようになります。このモードが有効になっている間は、ビジュアライザーやシステムオーディオのキャプチャ機能は動作しません",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence の更新間隔",
|
||||
"themeLight": "テーマ (ライト)",
|
||||
"fontType_optionBuiltIn": "組み込みフォント",
|
||||
@@ -271,7 +273,7 @@
|
||||
"customCss": "カスタム CSS",
|
||||
"customCssEnable_description": "カスタム CSS の記述を許可します",
|
||||
"customCssEnable": "カスタム CSS を有効にする",
|
||||
"customCssNotice": "警告: ある程度のサニタイズ (url() と content: の禁止) はありますが、カスタム CSS を使用するとインターフェースの変更によりリスクが生じる可能性があります",
|
||||
"customCssNotice": "警告: ある程度のサニタイズ (URL() と content: の禁止) はありますが、カスタム CSS を使用するとインターフェースの変更によりリスクが生じる可能性があります",
|
||||
"releaseChannel_optionBeta": "ベータ",
|
||||
"releaseChannel_optionLatest": "最新",
|
||||
"releaseChannel": "リリースチャンネル",
|
||||
@@ -314,7 +316,6 @@
|
||||
"exportImportSettings_importSuccess": "設定が正常にインポートされました!",
|
||||
"exportImportSettings_importModalTitle": "Feishin 設定をインポート",
|
||||
"exportImportSettings_importBtn": "設定をインポート",
|
||||
"autoDJ_description": "類似の曲を自動でキューに追加します",
|
||||
"autoDJ": "自動 DJ",
|
||||
"autoDJ_itemCount_description": "自動 DJ が有効なときにキューに追加しようとした曲数",
|
||||
"autoDJ_itemCount": "曲数",
|
||||
@@ -425,12 +426,26 @@
|
||||
"sidePlayQueueLayout_optionHorizontal": "水平",
|
||||
"sidePlayQueueLayout_optionVertical": "垂直",
|
||||
"waveformLoadingDelay": "波形読み込みの遅延",
|
||||
"waveformLoadingDelay_description": "波形を読み込むまでの遅延時間(秒単位)を設定します。Web プレーヤー使用時にカクつきが発生する場合は、この値を増やしてください。"
|
||||
"waveformLoadingDelay_description": "波形を読み込むまでの遅延時間(秒単位)を設定します。Web プレーヤー使用時にカクつきが発生する場合は、この値を増やしてください。",
|
||||
"playerbarWaveformStretch": "波形伸縮",
|
||||
"playerbarWaveformStretch_description": "波形を伸縮させて、利用可能なスペースを埋めます",
|
||||
"preventSuspendOnPlayback_description": "音楽再生中にアプリケーションが停止しないようにします",
|
||||
"preventSuspendOnPlayback": "再生の中断を防止する",
|
||||
"hotkey_listShowPlayingSong": "再生中の曲をリストに表示",
|
||||
"autoDJ_mode": "モード",
|
||||
"autoDJ_mode_albums": "アルバム",
|
||||
"autoDJ_mode_description": "キューに曲を追加するか、アルバム全体を追加するかを選択してください。",
|
||||
"autoDJ_mode_songs": "曲",
|
||||
"autoDJ_enabled": "Auto DJを有効にする",
|
||||
"autoDJ_albumStrategy": "アルバム選択モード",
|
||||
"autoDJ_songStrategy": "選曲モード",
|
||||
"autoDJ_strategy_option_library_random": "ランダム",
|
||||
"autoDJ_strategy_option_similar": "類似"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "$t(entity.playlist, {\"count\": 1}) を編集",
|
||||
"goToPage": "ページへ移動",
|
||||
"moveToTop": "先頭に移動",
|
||||
"moveToTop": "一番上へ移動",
|
||||
"clearQueue": "キューをクリア",
|
||||
"addToFavorites": "$t(entity.favorite, {\"count\": 2}) に追加",
|
||||
"addToPlaylist": "$t(entity.playlist, {\"count\": 1}) に追加",
|
||||
@@ -441,9 +456,9 @@
|
||||
"deletePlaylist": "$t(entity.playlist, {\"count\": 1}) を削除",
|
||||
"removeFromQueue": "キューから削除",
|
||||
"deselectAll": "すべて選択解除",
|
||||
"moveToBottom": "末尾に移動",
|
||||
"setRating": "評価を設定する",
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) エディタの切り替え",
|
||||
"moveToBottom": "一番下へ移動",
|
||||
"setRating": "評価を設定",
|
||||
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) エディターを切り替え",
|
||||
"removeFromFavorites": "$t(entity.favorite, {\"count\": 2}) から削除",
|
||||
"openIn": {
|
||||
"lastfm": "Last.fm で開く",
|
||||
@@ -452,9 +467,9 @@
|
||||
"listenbrainz": "ListenBrainz で開く",
|
||||
"qobuz": "Qobuz で開く"
|
||||
},
|
||||
"moveToNext": "次",
|
||||
"moveToNext": "次へ進む",
|
||||
"downloadStarted": "{{count}} 曲のダウンロードを開始しました",
|
||||
"moveItems": "曲を移動",
|
||||
"moveItems": "項目を移動",
|
||||
"shuffle": "シャッフル",
|
||||
"shuffleAll": "すべてシャッフル",
|
||||
"shuffleSelected": "選択した曲をシャッフル",
|
||||
@@ -466,26 +481,28 @@
|
||||
"moveDown": "下に移動",
|
||||
"holdToMoveToTop": "押し続けると一番上に移動します",
|
||||
"holdToMoveToBottom": "押し続けると一番下に移動します",
|
||||
"openApplicationDirectory": "アプリケーションディレクトリを開く",
|
||||
"openApplicationDirectory": "アプリディレクトリを開く",
|
||||
"selectRangeOfItems": "項目の範囲を選択",
|
||||
"addOrRemoveFromSelection": "選択に追加または削除",
|
||||
"goToCurrent": "現在の項目へ移動"
|
||||
"addOrRemoveFromSelection": "選択に追加または選択から除外",
|
||||
"goToCurrent": "現在の項目へ移動",
|
||||
"collapseAllFolders": "すべてのフォルダーを折りたたむ",
|
||||
"expandAllFolders": "すべてのフォルダーを展開する"
|
||||
},
|
||||
"common": {
|
||||
"backward": "戻る",
|
||||
"backward": "逆行",
|
||||
"increase": "増加",
|
||||
"rating": "評価",
|
||||
"bpm": "BPM",
|
||||
"refresh": "再読み込み",
|
||||
"unknown": "不明",
|
||||
"areYouSure": "実行しますか?",
|
||||
"areYouSure": "実行してもよろしいですか?",
|
||||
"edit": "編集",
|
||||
"favorite": "お気に入り",
|
||||
"left": "左側",
|
||||
"save": "保存",
|
||||
"right": "右側",
|
||||
"currentSong": "現在の $t(entity.track, {\"count\": 1})",
|
||||
"collapse": "折りたたみ",
|
||||
"collapse": "折りたたむ",
|
||||
"trackNumber": "トラック",
|
||||
"descending": "降順",
|
||||
"add": "追加",
|
||||
@@ -501,7 +518,7 @@
|
||||
"name": "名前",
|
||||
"maximize": "最大化",
|
||||
"decrease": "減少",
|
||||
"ok": "OK",
|
||||
"ok": "Ok",
|
||||
"description": "説明",
|
||||
"configure": "設定",
|
||||
"path": "パス",
|
||||
@@ -527,7 +544,7 @@
|
||||
"confirm": "確認",
|
||||
"resetToDefault": "デフォルトにリセット",
|
||||
"home": "ホーム",
|
||||
"comingSoon": "近日利用可能になる予定です…",
|
||||
"comingSoon": "近日公開…",
|
||||
"reset": "リセット",
|
||||
"channel_other": "チャンネル",
|
||||
"disable": "無効",
|
||||
@@ -536,7 +553,7 @@
|
||||
"menu": "メニュー",
|
||||
"restartRequired": "再起動が必要です",
|
||||
"previousSong": "前の $t(entity.track, {\"count\": 1})",
|
||||
"noResultsFromQuery": "条件にマッチするものがありません",
|
||||
"noResultsFromQuery": "クエリに一致する結果がありません",
|
||||
"quit": "終了",
|
||||
"expand": "展開",
|
||||
"search": "検索",
|
||||
@@ -546,11 +563,11 @@
|
||||
"random": "ランダム",
|
||||
"size": "サイズ",
|
||||
"biography": "バイオグラフィー",
|
||||
"note": "ノート",
|
||||
"note": "注記",
|
||||
"explicitStatus": "明示的なステータス",
|
||||
"additionalParticipants": "追加参加者",
|
||||
"newVersion": "新しいバージョン ({{version}}) がインストールされました",
|
||||
"viewReleaseNotes": "リリースノートを表示する",
|
||||
"viewReleaseNotes": "リリースノートを表示",
|
||||
"bitDepth": "ビット深度",
|
||||
"close": "閉じる",
|
||||
"codec": "コーデック",
|
||||
@@ -558,7 +575,7 @@
|
||||
"sampleRate": "サンプルレート",
|
||||
"preview": "プレビュー",
|
||||
"private": "プライベート",
|
||||
"public": "パブリック",
|
||||
"public": "公開",
|
||||
"share": "共有",
|
||||
"tags": "タグ",
|
||||
"trackGain": "トラックゲイン",
|
||||
@@ -590,7 +607,9 @@
|
||||
"rename": "名前を変更",
|
||||
"newVersionAvailable": "新しいバージョンが利用可能です",
|
||||
"numberOfResults": "{{numberOfResults}} 件の結果",
|
||||
"grouping": "グループ化"
|
||||
"grouping": "グループ化",
|
||||
"back": "戻る",
|
||||
"openFolder": "フォルダーを開く"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -677,7 +696,7 @@
|
||||
"rating": "評価",
|
||||
"favorite": "お気に入り",
|
||||
"playCount": "再生回数",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2})",
|
||||
"albumCount": "アルバム",
|
||||
"releaseYear": "年",
|
||||
"lastPlayed": "最後に再生",
|
||||
"biography": "バイオグラフィー",
|
||||
@@ -686,23 +705,23 @@
|
||||
"title": "タイトル",
|
||||
"bpm": "BPM",
|
||||
"dateAdded": "追加日",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||
"artist": "アーティスト",
|
||||
"songCount": "トラック",
|
||||
"trackNumber": "トラック",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"genre": "ジャンル",
|
||||
"albumArtist": "アルバムアーティスト",
|
||||
"path": "パス",
|
||||
"discNumber": "ディスク",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)",
|
||||
"channels": "チャンネル",
|
||||
"size": "サイズ",
|
||||
"codec": "コーデック",
|
||||
"bitDepth": "ビット深度",
|
||||
"sampleRate": "サンプリング周波数",
|
||||
"owner": "所有者"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "新たなポート設定を適用するためサーバーを再起動してください",
|
||||
"remotePortWarning": "新しいポートの設定を反映させるには、サーバーを再起動してください",
|
||||
"systemFontError": "システムフォントを取得する際にエラーが発生しました",
|
||||
"playbackError": "メディアの再生開始時にエラーが発生しました",
|
||||
"remotePortError": "リモートサーバーのポート設定時にエラーが発生しました",
|
||||
@@ -717,7 +736,7 @@
|
||||
"serverNotSelectedError": "サーバーが選択されていません",
|
||||
"remoteDisableError": "リモートサーバーを$t(common.disable)にする際にエラーが発生しました",
|
||||
"mpvRequired": "MPV が必要です",
|
||||
"audioDeviceFetchError": "オーディオデバイスの取得時にエラーが発生しました",
|
||||
"audioDeviceFetchError": "オーディオデバイスを取得しようとした際にエラーが発生しました",
|
||||
"invalidServer": "無効なサーバー",
|
||||
"loginRateError": "ログイン試行回数が多すぎます。数秒後に再試行してください",
|
||||
"endpointNotImplementedError": "{{serverType}} にはエンドポイント {{endpoint}} が実装されていません",
|
||||
@@ -725,7 +744,7 @@
|
||||
"networkError": "ネットワークエラーが発生しました",
|
||||
"notificationDenied": "通知の許可が拒否されました。この設定は効果がありません",
|
||||
"openError": "ファイルを開けませんでした",
|
||||
"badValue": "無効なオプション「{{value}}」。この値は存在しません",
|
||||
"badValue": "無効なオプション「{{value}}」です。この値は存在しません",
|
||||
"multipleServerSaveQueueError": "再生キューに現在のサーバーに存在しない曲が 1 曲以上あります。これはサポートされていません",
|
||||
"noNetwork": "サーバーが利用できません",
|
||||
"noNetworkDescription": "このサーバーに接続できませんでした",
|
||||
@@ -808,7 +827,7 @@
|
||||
"dynamicBackground": "ダイナミック背景",
|
||||
"synchronized": "同期",
|
||||
"followCurrentLyric": "歌詞を再生位置に追従",
|
||||
"opacity": "非透過率",
|
||||
"opacity": "不透明度",
|
||||
"lyricSize": "歌詞のサイズ",
|
||||
"showLyricProvider": "歌詞の提供元を表示",
|
||||
"unsynchronized": "非同期",
|
||||
@@ -817,7 +836,9 @@
|
||||
"lyricGap": "歌詞の間隔",
|
||||
"dynamicImageBlur": "画像のぼかしサイズ",
|
||||
"dynamicIsImage": "背景画像を有効にする",
|
||||
"lyricOffset": "歌詞のオフセット (ms)"
|
||||
"lyricOffset": "歌詞のオフセット (ms)",
|
||||
"lyricOpacityNonActive": "非アクティブな歌詞の不透明度",
|
||||
"lyricScaleNonActive": "非アクティブな歌詞のスケール"
|
||||
},
|
||||
"upNext": "次へ",
|
||||
"lyrics": "歌詞",
|
||||
@@ -1033,7 +1054,8 @@
|
||||
"input_skipDuplicates": "重複をスキップ",
|
||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"create": "$t(entity.playlist, {\"count\": 1}) {{playlist}} を作成する",
|
||||
"searchOrCreate": "$t(entity.playlist, {\"count\": 2}) を検索するか、入力して新しいプレイリストを作成してください"
|
||||
"searchOrCreate": "$t(entity.playlist, {\"count\": 2}) を検索するか、入力して新しいプレイリストを作成してください",
|
||||
"noneAdded": "$t(entity.playlist, {\"count\": 1}) {{playlist}} にトラックが追加されませんでした"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "サーバーをアップデート",
|
||||
@@ -1098,7 +1120,12 @@
|
||||
"input_played_optionAll": "すべてのトラック",
|
||||
"input_played_optionUnplayed": "未再生のトラックのみ",
|
||||
"input_played_optionPlayed": "再生されたトラックのみ",
|
||||
"input_played": "再生フィルター"
|
||||
"input_played": "再生フィルター",
|
||||
"input_kind_albums": "アルバム",
|
||||
"input_kind_songs": "曲",
|
||||
"input_kind": "ランダムピック",
|
||||
"input_limit_albums": "アルバムは何枚ですか?",
|
||||
"input_limit_songs": "何曲ですか?"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "プレイキューをサーバーに保存しました"
|
||||
@@ -1341,6 +1368,7 @@
|
||||
"systemAudioConsentDecline": "拒否",
|
||||
"systemAudioConsentTitle": "システムオーディオへのアクセスを許可しますか?",
|
||||
"systemAudioCaptureFailed": "キャプチャを開始できませんでした: {{message}}",
|
||||
"systemAudioNoAudioTrack": "音声トラックが返されませんでした。プロンプトが表示されたら、音声キャプチャが有効になっていることを確認してください。"
|
||||
"systemAudioNoAudioTrack": "音声トラックが返されませんでした。プロンプトが表示されたら、音声キャプチャが有効になっていることを確認してください。",
|
||||
"systemAudioExclusiveModeNotSupported": "オーディオ排他モードが有効になっている間は、ビジュアライザーは利用できません。MPV の設定でオーディオ排他モードを無効にしてから、もう一度お試しください。"
|
||||
}
|
||||
}
|
||||
|
||||
+250
-30
@@ -17,7 +17,10 @@
|
||||
"removeFromPlaylist": "$t(entity.playlist, {\"count\": 1})에서 제거",
|
||||
"openIn": {
|
||||
"musicbrainz": "MusicBrainz에서 보기",
|
||||
"lastfm": "Last.fm에서 보기"
|
||||
"lastfm": "Last.fm에서 보기",
|
||||
"listenbrainz": "ListenBrainz에서 열기",
|
||||
"qobuz": "Qobuz에서 열기",
|
||||
"spotify": "Spotify에서 열기"
|
||||
},
|
||||
"viewPlaylists": "$t(entity.playlist, {\"count\": 2}) 보기",
|
||||
"setRating": "평점 지정",
|
||||
@@ -37,7 +40,10 @@
|
||||
"shuffleAll": "모두 섞기",
|
||||
"shuffleSelected": "선택항목 섞기",
|
||||
"viewMore": "더 보기",
|
||||
"openApplicationDirectory": "앱 디렉토리 열기"
|
||||
"openApplicationDirectory": "앱 디렉토리 열기",
|
||||
"goToCurrent": "현재 항목으로 이동",
|
||||
"collapseAllFolders": "모든 폴더 접기",
|
||||
"expandAllFolders": "모든 폴더 확장"
|
||||
},
|
||||
"common": {
|
||||
"translation": "번역",
|
||||
@@ -106,7 +112,7 @@
|
||||
"ascending": "오름차순",
|
||||
"areYouSure": "확실한가요?",
|
||||
"bitrate": "비트 전송률",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"biography": "바이오그래피",
|
||||
"center": "중앙",
|
||||
"channel_other": "채널",
|
||||
@@ -127,7 +133,7 @@
|
||||
"filters": "필터",
|
||||
"noResultsFromQuery": "쿼리 결과가 없습니다",
|
||||
"note": "노트",
|
||||
"ok": "OK",
|
||||
"ok": "Ok",
|
||||
"owner": "소유자",
|
||||
"sampleRate": "샘플레이트",
|
||||
"tags": "태그",
|
||||
@@ -149,7 +155,18 @@
|
||||
"sort": "정렬",
|
||||
"gridRows": "행 그리드",
|
||||
"tableColumns": "테이블 열",
|
||||
"itemsMore": "{{count}}개 더"
|
||||
"itemsMore": "{{count}}개 더",
|
||||
"back": "뒤로",
|
||||
"example": "예",
|
||||
"openFolder": "폴더 열기",
|
||||
"filter_single": "미혼",
|
||||
"filter_multiple": "다중",
|
||||
"grouping": "그룹화",
|
||||
"mood": "기분",
|
||||
"numberOfResults": "결과 {{numberOfResults}}개",
|
||||
"retry": "다시 해 보다",
|
||||
"rename": "이름 변경",
|
||||
"newVersionAvailable": "새로운 버전이 나왔습니다"
|
||||
},
|
||||
"entity": {
|
||||
"albumWithCount_other": "{{count}} 앨범",
|
||||
@@ -197,7 +214,15 @@
|
||||
"localFontAccessDenied": "로컬 글꼴에 접근 거부되었습니다",
|
||||
"apiRouteError": "요청 보내기 실패",
|
||||
"badValue": "옵션이 없습니다 {{value}}. 이 값은 더이상 존재하지 않습니다",
|
||||
"notificationDenied": "알림에 대한 권한이 거부되었습니다. 이 설정은 변경되지 않습니다"
|
||||
"notificationDenied": "알림에 대한 권한이 거부되었습니다. 이 설정은 변경되지 않습니다",
|
||||
"invalidJson": "유효하지 않은 JSON",
|
||||
"multipleServerSaveQueueError": "재생 대기열에 현재 서버에 속하지 않은 곡이 하나 이상 포함되어 있습니다. 이는 지원되지 않습니다",
|
||||
"noNetwork": "서버를 이용할 수 없음",
|
||||
"noNetworkDescription": "이 서버에 연결할 수 없습니다",
|
||||
"playbackPausedDueToError": "오류로 인해 재생이 일시 중지되었습니다",
|
||||
"saveQueueFailed": "큐 저장 실패",
|
||||
"serverLockSingleServer": "서버가 잠겨 있을 때는 서버를 하나만 허용합니다",
|
||||
"settingsSyncError": "렌더러와 메인 프로세스의 설정 간에 불일치가 발견되었습니다. 변경 사항을 적용하려면 애플리케이션을 다시 시작하십시오"
|
||||
},
|
||||
"filter": {
|
||||
"title": "곡명",
|
||||
@@ -222,9 +247,9 @@
|
||||
"disc": "디스크",
|
||||
"bitrate": "비트 전송률",
|
||||
"biography": "바이오그래피",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"duration": "길이",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2}) 앨범수",
|
||||
"comment": "코멘트",
|
||||
"favorited": "즐겨찾기",
|
||||
@@ -242,7 +267,10 @@
|
||||
"songCount": "곡 갯수",
|
||||
"toYear": "년도까지",
|
||||
"trackNumber": "트랙",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
"explicitStatus": "$t(common.explicitStatus)",
|
||||
"matchAnd": "그리고",
|
||||
"matchOr": "또는",
|
||||
"sortName": "이름 정렬"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
@@ -251,14 +279,17 @@
|
||||
"input_name": "서버 이름",
|
||||
"input_password": "비밀번호",
|
||||
"input_savePassword": "비밀번호 저장하기",
|
||||
"input_url": "url",
|
||||
"input_url": "URL",
|
||||
"error_savePassword": "비밀번호를 저장하는 도중 오류가 발생했습니다",
|
||||
"ignoreCors": "CORS 무시 ($t(common.restartRequired))",
|
||||
"ignoreSsl": "SSL 무시 ($t(common.restartRequired))",
|
||||
"input_legacyAuthentication": "레거시 인증 사용",
|
||||
"input_username": "유저 이름",
|
||||
"input_preferInstantMix": "즉석 믹스 선호",
|
||||
"input_preferInstantMixDescription": "비슷한 곳을 찾기 위해 즉석 믹스를 사용합니다. 이 명령을 수정하기 위한 플러그인을 설치한 경우 유용합니다"
|
||||
"input_preferInstantMixDescription": "비슷한 곳을 찾기 위해 즉석 믹스를 사용합니다. 이 명령을 수정하기 위한 플러그인을 설치한 경우 유용합니다",
|
||||
"input_preferRemoteUrl": "공개 URL 선호",
|
||||
"input_remoteUrl": "공개 URL",
|
||||
"input_remoteUrlPlaceholder": "선택 사항: 외부 기능을 위한 공개 URL"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"input_skipDuplicates": "중복 건너뛰기",
|
||||
@@ -266,7 +297,8 @@
|
||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"success": "$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })에 $t(entity.trackWithCount, {\"count\": {{message}} })가 추가되었습니다",
|
||||
"create": "$t(entity.playlist, {\"count\": 1}) {{playlist}} 생성",
|
||||
"searchOrCreate": "$t(entity.playlist, {\"count\": 2}) 검색 또는 입력하여 새로 만들기"
|
||||
"searchOrCreate": "$t(entity.playlist, {\"count\": 2}) 검색 또는 입력하여 새로 만들기",
|
||||
"noneAdded": "$t(entity.playlist, {\"count\": 1}) '{{playlist}}'에 트랙이 추가되지 않았습니다"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"title": "가사 검색",
|
||||
@@ -276,7 +308,11 @@
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "모두 일치",
|
||||
"input_optionMatchAny": "무엇이든 일치",
|
||||
"title": "쿼리 편집기"
|
||||
"title": "쿼리 편집기",
|
||||
"addRuleGroup": "규칙 그룹 추가",
|
||||
"removeRuleGroup": "규칙 그룹 제거",
|
||||
"resetToDefault": "기본값으로 초기화",
|
||||
"clearFilters": "필터 초기화"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "$t(entity.playlist, {\"count\": 1}) 편집",
|
||||
@@ -289,7 +325,9 @@
|
||||
"success": "클립보드에 공유 링크를 복사했습니다 (또는 열어보려면 클릭하세요)",
|
||||
"expireInvalid": "만료 날짜는 미래 날짜여야만 합니다",
|
||||
"createFailed": "공유 링크를 생성하는데 실패하였습니다 (혹시 공유하기 설정되어 있나요?)",
|
||||
"setExpiration": "만료 기간 설정하기"
|
||||
"setExpiration": "만료 기간 설정하기",
|
||||
"copyToClipboard": "클립보드로 복사: Ctrl+C, Enter",
|
||||
"successMustClick": "공유가 성공적으로 생성되었습니다. 여기를 클릭하여 여세요"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "서버 업데이트",
|
||||
@@ -312,6 +350,44 @@
|
||||
"enabled": "프라이빗 모드가 활성화되었습니다. 재생상태가 외부 서비스에 지금부터 노출되지 않습니다",
|
||||
"disabled": "프라이빗 모드가 비활성화되었습니다. 재생상태가 외부서비스에서 지금부터 표시됩니다",
|
||||
"title": "프라이빗 모드"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "대기열에 항목을 추가하세요",
|
||||
"description": "이 작업은 현재 필터링된 보기의 모든 항목을 추가합니다"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "라디오 방송국이 성공적으로 생성되었습니다",
|
||||
"title": "라디오 방송국 만들기",
|
||||
"input_homepageUrl": "홈페이지 URL",
|
||||
"input_name": "명의",
|
||||
"input_streamUrl": "스트림 URL"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "라디오 방송국이 성공적으로 업데이트되었습니다"
|
||||
},
|
||||
"lyricsExport": {
|
||||
"export": "가사 내보내기",
|
||||
"input_synced": "동기화된 가사 내보내기",
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
},
|
||||
"saveQueue": {
|
||||
"success": "재생 대기열을 서버에 저장했습니다"
|
||||
},
|
||||
"shuffleAll": {
|
||||
"title": "무작위 재생",
|
||||
"input_kind_albums": "앨범",
|
||||
"input_kind_songs": "노래들",
|
||||
"input_kind": "무작위 선택",
|
||||
"input_limit_albums": "앨범이 몇 장인가요?",
|
||||
"input_limit_songs": "몇 곡인가요?",
|
||||
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"input_limit": "몇 곡인가요?",
|
||||
"input_minYear": "연도부터",
|
||||
"input_maxYear": "연도까지",
|
||||
"input_played": "재생 필터",
|
||||
"input_played_optionAll": "모든 트랙",
|
||||
"input_played_optionUnplayed": "재생하지 않은 트랙만",
|
||||
"input_played_optionPlayed": "재생된 트랙만"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
@@ -325,7 +401,13 @@
|
||||
"collapseSidebar": "사이드바 줄이기",
|
||||
"expandSidebar": "사이드바 확장",
|
||||
"privateModeOff": "프라이빗 모드 끄기",
|
||||
"privateModeOn": "프라이빗 모드 켜기"
|
||||
"privateModeOn": "프라이빗 모드 켜기",
|
||||
"commandPalette": "명령 팔레트 열기",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectMusicFolder": "음악 폴더 선택",
|
||||
"noMusicFolder": "음악 폴더가 선택되지 않았습니다",
|
||||
"multipleMusicFolders": "{{count}}개의 음악 폴더가 선택되었습니다",
|
||||
"settings": "$t(common.setting, {\"count\": 2})"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "서버 설정하기",
|
||||
@@ -350,7 +432,9 @@
|
||||
"lyricGap": "가사 간격",
|
||||
"lyricSize": "가사 크기",
|
||||
"showLyricMatch": "가사 일치 표시",
|
||||
"showLyricProvider": "가사 제공자 표시"
|
||||
"showLyricProvider": "가사 제공자 표시",
|
||||
"lyricOpacityNonActive": "비활성 가사 불투명도",
|
||||
"lyricScaleNonActive": "비활성 서정적 척도"
|
||||
},
|
||||
"lyrics": "가사",
|
||||
"related": "관련",
|
||||
@@ -364,7 +448,27 @@
|
||||
"shareItem": "공유",
|
||||
"goToAlbum": "$t(entity.album, {\"count\": 1})으로 이동",
|
||||
"goToAlbumArtist": "$t(entity.albumArtist, {\"count\": 1})으로 이동",
|
||||
"showDetails": "추가정보"
|
||||
"showDetails": "추가정보",
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
"addLast": "$t(player.addLast)",
|
||||
"addNext": "$t(player.addNext)",
|
||||
"addToFavorites": "$t(action.addToFavorites)",
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
"createPlaylist": "$t(action.createPlaylist)",
|
||||
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||
"deselectAll": "$t(action.deselectAll)",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"moveToBottom": "$t(action.moveToBottom)",
|
||||
"moveToTop": "$t(action.moveToTop)",
|
||||
"play": "$t(player.play)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"removeFromFavorites": "$t(action.removeFromFavorites)",
|
||||
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"goTo": "이동"
|
||||
},
|
||||
"albumArtistDetail": {
|
||||
"about": "{{artist}}에 대해",
|
||||
@@ -375,7 +479,13 @@
|
||||
"topSongs": "최고의 곡들",
|
||||
"topSongsFrom": "{{title}}이 포함된 최고의 곡들",
|
||||
"viewAll": "전부 보이기",
|
||||
"viewAllTracks": "$t(entity.track, {\"count\": 2}) 전부 보이기"
|
||||
"viewAllTracks": "$t(entity.track, {\"count\": 2}) 전부 보이기",
|
||||
"favoriteSongs": "좋아하는 노래들",
|
||||
"groupingTypeAll": "모든 릴리스 유형",
|
||||
"groupingTypePrimary": "주요 릴리스 유형",
|
||||
"topSongsCommunity": "공동체",
|
||||
"topSongsPersonal": "개인의",
|
||||
"favoriteSongsFrom": "{{title}}에서 가장 좋아하는 곡들"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||
@@ -386,11 +496,14 @@
|
||||
"released": "발매"
|
||||
},
|
||||
"albumList": {
|
||||
"artistAlbums": "{{artist}}의 앨범"
|
||||
"artistAlbums": "{{artist}}의 앨범",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})",
|
||||
"title": "$t(entity.album, {\"count\": 2})"
|
||||
},
|
||||
"genreList": {
|
||||
"showAlbums": "$t(entity.genre, {\"count\": 1}) $t(entity.album, {\"count\": 2}) 표시",
|
||||
"showTracks": "$t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2}) 표시"
|
||||
"showTracks": "$t(entity.genre, {\"count\": 1}) $t(entity.track, {\"count\": 2}) 표시",
|
||||
"title": "$t(entity.genre, {\"count\": 2})"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
@@ -405,7 +518,9 @@
|
||||
"mostPlayed": "자주 플레이된 곡",
|
||||
"newlyAdded": "최근에 추가된 곡",
|
||||
"recentlyPlayed": "최근에 플레이된 곡",
|
||||
"recentlyReleased": "최근에 발매된 곡"
|
||||
"recentlyReleased": "최근에 발매된 곡",
|
||||
"genres": "$t(entity.genre, {\"count\": 2})",
|
||||
"title": "$t(common.home)"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "클립보드에 경로를 복사",
|
||||
@@ -420,15 +535,71 @@
|
||||
"generalTab": "일반",
|
||||
"hotkeysTab": "단축키",
|
||||
"playbackTab": "재생",
|
||||
"windowTab": "윈도우"
|
||||
"windowTab": "윈도우",
|
||||
"analytics": "해석학",
|
||||
"updates": "업데이트",
|
||||
"cache": "은닉처",
|
||||
"application": "애플리케이션",
|
||||
"queryBuilder": "쿼리 빌더",
|
||||
"theme": "테마",
|
||||
"controls": "통제 수단",
|
||||
"sidebar": "사이드바",
|
||||
"exportImport": "가져오기/내보내기",
|
||||
"audio": "오디오",
|
||||
"lyrics": "가사",
|
||||
"lyricsDisplay": "가사 표시",
|
||||
"transcoding": "트랜스코딩",
|
||||
"discord": "Discord",
|
||||
"logger": "로거",
|
||||
"playerFilters": "선수 필터"
|
||||
},
|
||||
"sidebar": {
|
||||
"myLibrary": "내 라이브러리",
|
||||
"nowPlaying": "재생중",
|
||||
"shared": "공유 $t(entity.playlist, {\"count\": 2})"
|
||||
"shared": "공유 $t(entity.playlist, {\"count\": 2})",
|
||||
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
|
||||
"albums": "$t(entity.album, {\"count\": 2})",
|
||||
"collections": "컬렉션",
|
||||
"artists": "$t(entity.artist, {\"count\": 2})",
|
||||
"favorites": "$t(entity.favorite, {\"count\": 2})",
|
||||
"folders": "$t(entity.folder, {\"count\": 2})",
|
||||
"genres": "$t(entity.genre, {\"count\": 2})",
|
||||
"home": "$t(common.home)",
|
||||
"radio": "$t(entity.radioStation, {\"count\": 2})",
|
||||
"playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting, {\"count\": 2})",
|
||||
"tracks": "$t(entity.track, {\"count\": 2})"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "{{artist}}의 음악"
|
||||
"artistTracks": "{{artist}}의 음악",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
|
||||
"title": "$t(entity.track, {\"count\": 2})"
|
||||
},
|
||||
"radioList": {
|
||||
"title": "라디오 방송국"
|
||||
},
|
||||
"releasenotes": {
|
||||
"commitsSinceStable": "{{stable}} 이후 커밋",
|
||||
"noNewCommits": "이 범위에 새로운 커밋이 없습니다",
|
||||
"noStableReleaseToCompare": "비교할 수 있는 안정화 릴리스가 없습니다"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "$t(entity.favorite, {\"count\": 2})"
|
||||
},
|
||||
"windowBar": {
|
||||
"paused": "(일시 정지됨) ",
|
||||
"privateMode": "(비공개 모드)"
|
||||
},
|
||||
"folderList": {
|
||||
"title": "$t(entity.folder, {\"count\": 2})"
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist, {\"count\": 2})"
|
||||
},
|
||||
"collections": {
|
||||
"overrideExisting": "기존 항목 덮어쓰기",
|
||||
"saveAsCollection": "컬렉션으로 저장"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -458,8 +629,8 @@
|
||||
"playSimilarSongs": "비슷한 곡 재생",
|
||||
"previous": "이전",
|
||||
"queue_clear": "재생 대기열 지우기",
|
||||
"queue_moveToBottom": "선택한 곡을 가장 위로 이동",
|
||||
"queue_moveToTop": "선택한 곡을 가장 아래로 이동",
|
||||
"queue_moveToBottom": "선택한 곡을 가장 아래로 이동",
|
||||
"queue_moveToTop": "선택한 곡을 가장 위로 이동",
|
||||
"queue_remove": "선택한 항목 삭제",
|
||||
"repeat": "반복",
|
||||
"repeat_all": "모두 반복하기",
|
||||
@@ -473,7 +644,25 @@
|
||||
"toggleFullscreenPlayer": "전체화면으로 전환",
|
||||
"unfavorite": "즐겨찾기 취소",
|
||||
"pause": "멈춤",
|
||||
"viewQueue": "대기열 보기"
|
||||
"viewQueue": "대기열 보기",
|
||||
"addLastShuffled": "마지막 (섞인)",
|
||||
"addNextShuffled": "다음 (무작위)",
|
||||
"albumRadio": "앨범 라디오",
|
||||
"artistRadio": "아티스트 라디오",
|
||||
"holdToShuffle": "길게 눌러 섞기",
|
||||
"lyrics": "가사",
|
||||
"restoreQueueFromServer": "서버에서 큐 복원",
|
||||
"saveQueueToServer": "대기열을 서버에 저장",
|
||||
"trackRadio": "라디오 추적",
|
||||
"sleepTimer": "취침 타이머",
|
||||
"sleepTimer_endOfSong": "현재 곡 종료",
|
||||
"sleepTimer_endOfAlbum": "현재 앨범의 끝",
|
||||
"sleepTimer_minutes": "{{count}}분",
|
||||
"sleepTimer_hours": "{{count}}시간",
|
||||
"sleepTimer_off": "끄다",
|
||||
"sleepTimer_timeRemaining": "{{time}} 남음",
|
||||
"sleepTimer_setCustom": "타이머 설정",
|
||||
"sleepTimer_cancel": "타이머 취소"
|
||||
},
|
||||
"setting": {
|
||||
"accentColor_description": "앱의 강조색상 설정",
|
||||
@@ -482,7 +671,7 @@
|
||||
"albumBackground": "앨범 배경이미지",
|
||||
"albumBackgroundBlur_description": "앨범 배경이미지의 흐려짐 정도 조정",
|
||||
"albumBackgroundBlur": "앨범배경이미지 흐려짐 크기",
|
||||
"applicationHotkeys_description": "앱의 단축키 설정. 앱 전체에 적용되는 단축키를 설정하기 위해서는 체크박스에 체크하세요(PC만 가능)",
|
||||
"applicationHotkeys_description": "애플리케이션 단축키를 설정합니다. 체크박스를 전환하여 전역 단축키로 설정하세요(데스크톱 전용)",
|
||||
"applicationHotkeys": "앱 단축키",
|
||||
"artistBackground": "아티스트 배경이미지",
|
||||
"artistBackground_description": "아티스트 페이지에 아티스트가 포함된 배경이미지를 추가",
|
||||
@@ -492,7 +681,7 @@
|
||||
"artistConfiguration_description": "앨범아티스트 페이지에 표시할 정보 및 순서 설정",
|
||||
"audioDevice_description": "음악재생에 사용할 장치 선택(웹플레이어만 가능)",
|
||||
"audioDevice": "오디오 장치",
|
||||
"audioExclusiveMode_description": "단독재생모드 켜기. 이 모드에서는 일반적으로 시스템의 재생장치가 고정되며 MPV로만 오디오가 재생됩니다",
|
||||
"audioExclusiveMode_description": "독점 출력 모드를 활성화합니다. 이 모드에서는 일반적으로 시스템의 오디오 출력이 차단되며, 오직 mpv만이 오디오를 출력할 수 있습니다. 이 모드가 활성화된 동안에는 비주얼라이저의 시스템 오디오 캡처 기능이 작동하지 않습니다",
|
||||
"audioExclusiveMode": "오디오 단독재생모드",
|
||||
"audioPlayer_description": "재생을 위한 오디오 플레이어 선택",
|
||||
"audioPlayer": "오디오 플레이어",
|
||||
@@ -505,7 +694,8 @@
|
||||
"broadcast": "방송",
|
||||
"ep": "ep앨범",
|
||||
"other": "기타",
|
||||
"single": "싱글"
|
||||
"single": "싱글",
|
||||
"album": "$t(entity.album, {\"count\": 1})"
|
||||
},
|
||||
"secondary": {
|
||||
"audiobook": "오디오북",
|
||||
@@ -521,5 +711,35 @@
|
||||
"soundtrack": "사운드트랙",
|
||||
"spokenWord": "보컬사운드"
|
||||
}
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "분",
|
||||
"secondShort": "초",
|
||||
"hourShort": "시간",
|
||||
"dayShort": "일"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "~ 뒤에 있나요",
|
||||
"afterDate": "(날짜) 이후입니까",
|
||||
"before": "~보다 앞서 있다",
|
||||
"beforeDate": "(날짜) 이전인가요",
|
||||
"contains": "포함",
|
||||
"endsWith": "~로 끝남",
|
||||
"inPlaylist": "~ 안에 있다",
|
||||
"inTheLast": "마지막에 있습니다",
|
||||
"inTheRange": "범위 내에 있습니다",
|
||||
"inTheRangeDate": "범위 내에 있음 (날짜)",
|
||||
"is": "~이다",
|
||||
"isNot": "~이 아닙니까",
|
||||
"isGreaterThan": "~보다 크다",
|
||||
"isLessThan": "~보다 작다",
|
||||
"matchesRegex": "정규식과 일치",
|
||||
"notContains": "함유하지 않음",
|
||||
"notInPlaylist": "~ 안에 있지 않다",
|
||||
"notInTheLast": "마지막에 있지 않다",
|
||||
"startsWith": "~로 시작함"
|
||||
},
|
||||
"queryBuilder": {
|
||||
"customTags": "사용자 정의 태그"
|
||||
}
|
||||
}
|
||||
|
||||
+494
-473
File diff suppressed because it is too large
Load Diff
+912
-903
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "Legg til i $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Legg til i $t(entity.playlist, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "Legg til eller fjern fra val",
|
||||
"selectRangeOfItems": "Vel eit utval av element",
|
||||
"clearQueue": "Tøm kø",
|
||||
"goToCurrent": "Gå til noverande element",
|
||||
"collapseAllFolders": "Lukk alle mapper",
|
||||
"expandAllFolders": "Opne alle mapper",
|
||||
"createPlaylist": "Lag $t(entity.playlist, {\"count\": 1})",
|
||||
"createRadioStation": "Lag $t(entity.radioStation, {\"count\": 1})",
|
||||
"deletePlaylist": "Slett $t(entity.playlist, {\"count\": 1})",
|
||||
"deleteRadioStation": "Slett $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "Vel alle",
|
||||
"deselectAll": "Opphev alle val",
|
||||
"downloadStarted": "Starta nedlasting av {{count}} element",
|
||||
"editPlaylist": "Rediger $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "Gå til side",
|
||||
"moveToNext": "Flytt til neste",
|
||||
"moveToBottom": "Flytt til botnen",
|
||||
"moveToTop": "Flytt til toppen",
|
||||
"moveUp": "Flytt opp",
|
||||
"moveDown": "Flytt ned",
|
||||
"holdToMoveToTop": "Held inne for å flytte til toppen",
|
||||
"holdToMoveToBottom": "Held inne for å flytte til botnen",
|
||||
"moveItems": "Flytt element",
|
||||
"shuffle": "Stokking",
|
||||
"shuffleAll": "Stokk alle",
|
||||
"shuffleSelected": "Stokk valde",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "Fjern frå $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "Fjern frå $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "Fjern frå kø",
|
||||
"setRating": "Set vurdering",
|
||||
"toggleSmartPlaylistEditor": "Slå $t(entity.smartPlaylist) editor av/på",
|
||||
"viewPlaylists": "Vis $t(entity.playlist, {\"count\": 2})",
|
||||
"viewMore": "Vis fleire",
|
||||
"openApplicationDirectory": "Opne applikasjonsmappa",
|
||||
"openIn": {
|
||||
"lastfm": "Opne i Last.fm",
|
||||
"listenbrainz": "Opne i ListenBrainz",
|
||||
"musicbrainz": "Opne i MusicBrainz",
|
||||
"qobuz": "Opne i Qobuz",
|
||||
"spotify": "Opne i Spotify"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"countSelected": "{{count}} valt",
|
||||
"explicitStatus": "Eksplisittstatus",
|
||||
"action_one": "Handling",
|
||||
"action_other": "Handlingar",
|
||||
"add": "Legg til",
|
||||
"additionalParticipants": "Ytterlegare deltakarar",
|
||||
"newVersion": "Ein ny versjon vart installert ({{version}})",
|
||||
"viewReleaseNotes": "Sjå utgivelsesnotata",
|
||||
"albumGain": "Albumforsterkning",
|
||||
"albumPeak": "Albumtopp",
|
||||
"areYouSure": "Er du sikker?",
|
||||
"ascending": "Stigande",
|
||||
"back": "Tilbake",
|
||||
"backward": "Bakover",
|
||||
"biography": "Biografi",
|
||||
"bitDepth": "Bit-dybde",
|
||||
"bitrate": "Bitrate",
|
||||
"bpm": "BPM",
|
||||
"cancel": "Avbryt",
|
||||
"center": "Sentrer",
|
||||
"channel_one": "Kanal",
|
||||
"channel_other": "Kanalar",
|
||||
"clear": "Tøm",
|
||||
"close": "Lukk",
|
||||
"codec": "Codec",
|
||||
"collapse": "Lukk",
|
||||
"comingSoon": "Kjem snart…",
|
||||
"configure": "Konfigurer",
|
||||
"confirm": "Bekreft",
|
||||
"create": "Lag",
|
||||
"currentSong": "Noverande $t(entity.track, {\"count\": 1})",
|
||||
"decrease": "Reduser",
|
||||
"delete": "Slett",
|
||||
"descending": "Søkkande",
|
||||
"description": "Beskriving",
|
||||
"disable": "Skru av",
|
||||
"disc": "Disk",
|
||||
"dismiss": "Avvis",
|
||||
"doNotShowAgain": "Ikkje vis dette igjen",
|
||||
"duration": "Varigheit",
|
||||
"view": "Vis",
|
||||
"edit": "Rediger",
|
||||
"enable": "Skru på",
|
||||
"expand": "Utvid",
|
||||
"example": "Døme",
|
||||
"externalLinks": "Eksterne lenker",
|
||||
"openFolder": "Open mappe",
|
||||
"faster": "Raskare",
|
||||
"favorite": "Favoriser",
|
||||
"filter_one": "Filter",
|
||||
"filter_other": "Filter",
|
||||
"filters": "Filter",
|
||||
"filter_single": "Single",
|
||||
"filter_multiple": "Multi",
|
||||
"forceRestartRequired": "Start på nytt for at endringane vert teke i bruk… lukk varslina for å starte på nytt",
|
||||
"forward": "Framover",
|
||||
"gap": "Mellomrom",
|
||||
"home": "Heim",
|
||||
"increase": "Auk",
|
||||
"left": "Venstre",
|
||||
"limit": "Grense",
|
||||
"manage": "Administrer",
|
||||
"maximize": "Maksimer",
|
||||
"menu": "Meny",
|
||||
"minimize": "Minimer",
|
||||
"modified": "Modifisert",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"grouping": "Gruppering",
|
||||
"mood": "Humør",
|
||||
"name": "Namn",
|
||||
"no": "Nei",
|
||||
"none": "Ingen",
|
||||
"noResultsFromQuery": "Søket gav ingen resultat",
|
||||
"numberOfResults": "{{numberOfResults}} resultat",
|
||||
"noFilters": "Ingen filter konfigurert",
|
||||
"note": "Notat",
|
||||
"ok": "Ok",
|
||||
"owner": "Eigar",
|
||||
"path": "Bane",
|
||||
"playerMustBePaused": "Spelaren må vere på pause",
|
||||
"preview": "Førehandsvisning",
|
||||
"previousSong": "Forrige $t(entity.track, {\"count\": 1})",
|
||||
"private": "Privat",
|
||||
"public": "Offentleg",
|
||||
"quit": "Lukk",
|
||||
"random": "Tilfeldig",
|
||||
"rating": "Vurdering",
|
||||
"retry": "Prøv på nytt",
|
||||
"recordLabel": "Plateselskap",
|
||||
"releaseType": "Utgjevingstype",
|
||||
"refresh": "Oppdater",
|
||||
"reload": "Oppdater",
|
||||
"rename": "Gje nytt namn",
|
||||
"reset": "Tilbakestill",
|
||||
"resetToDefault": "Tilbakestill til standard",
|
||||
"restartRequired": "Omstart nødvendig",
|
||||
"right": "Høgre",
|
||||
"sampleRate": "Samplingsfrekvens",
|
||||
"save": "Lagre",
|
||||
"saveAndReplace": "Lagre og erstatt",
|
||||
"saveAs": "Lagre som",
|
||||
"search": "Søk",
|
||||
"setting_one": "Instilling",
|
||||
"setting_other": "Instillingar",
|
||||
"slower": "Saktare",
|
||||
"share": "Del",
|
||||
"size": "Storleik",
|
||||
"sort": "Sorter",
|
||||
"sortOrder": "Rekkjefølgje",
|
||||
"tags": "Taggar",
|
||||
"title": "Tittel",
|
||||
"trackNumber": "Spor",
|
||||
"trackGain": "Sporforsterkning",
|
||||
"trackPeak": "Sporetopp",
|
||||
"translation": "Omsetjing",
|
||||
"unknown": "Ukjend",
|
||||
"version": "Versjon",
|
||||
"year": "År",
|
||||
"yes": "Ja",
|
||||
"explicit": "Eksplisitt",
|
||||
"clean": "Rein",
|
||||
"gridRows": "Rutenettrader",
|
||||
"tableColumns": "Tabellkolonnar",
|
||||
"itemsMore": "{{count}} fleire",
|
||||
"newVersionAvailable": "Ein ny versjon er tilgjengeleg"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "Album",
|
||||
"album_other": "Album",
|
||||
"albumArtist_one": "Albumartist",
|
||||
"albumArtist_other": "Albumartistar",
|
||||
"albumArtistCount_one": "{{count}} albumartist",
|
||||
"albumArtistCount_other": "{{count}} albumartistar",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_other": "{{count}} album",
|
||||
"radioStation_one": "Radiostasjon",
|
||||
"radioStation_other": "Radiostasjonar",
|
||||
"radioStationWithCount_one": "{{count}} radiostasjon",
|
||||
"radioStationWithCount_other": "{{count}} radiostasjonar",
|
||||
"artist_one": "Artist",
|
||||
"artist_other": "Artistar",
|
||||
"artistWithCount_one": "{{count}} artist",
|
||||
"artistWithCount_other": "{{count}} artistar",
|
||||
"favorite_one": "Favoritt",
|
||||
"favorite_other": "Favorittar",
|
||||
"folder_one": "Mappe",
|
||||
"folder_other": "Mapper",
|
||||
"folderWithCount_one": "{{count}} mappe",
|
||||
"folderWithCount_other": "{{count}} mapper",
|
||||
"genre_one": "Sjanger",
|
||||
"genre_other": "Sjangrar",
|
||||
"genreWithCount_one": "{{count}} sjanger",
|
||||
"genreWithCount_other": "{{count}} sjangrar",
|
||||
"playlist_one": "Speleliste",
|
||||
"playlist_other": "Spelelister",
|
||||
"play_one": "{{count}} avspeling",
|
||||
"play_other": "{{count}} avspelingar",
|
||||
"playlistWithCount_one": "{{count}} speleliste",
|
||||
"playlistWithCount_other": "{{count}} spelelister",
|
||||
"smartPlaylist": "Smart $t(entity.playlist, {\"count\": 1})",
|
||||
"track_one": "Spor",
|
||||
"track_other": "Spor",
|
||||
"song_one": "Song",
|
||||
"song_other": "Songar",
|
||||
"trackWithCount_one": "{{count}} spor",
|
||||
"trackWithCount_other": "{{count}} spor"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "Kunne ikkje rute førespurnaden",
|
||||
"audioDeviceFetchError": "Det oppstod ein feil under forsøk på å hente lydeiningar",
|
||||
"authenticationFailed": "Autentisering feila",
|
||||
"badAlbum": "Du ser denne sida fordi denne songen ikkje er ein del av eit album. Mest sannsynleg ser du dette fordi du har songen på toppen av musikkmappa di. Jellyfin grupperar berre spor viss dei er i ei mappe",
|
||||
"badValue": "Ugyldig val \"{{value}}\". Denne verdien finst ikkje lenger",
|
||||
"credentialsRequired": "Legitimasjon krevjast",
|
||||
"endpointNotImplementedError": "Endepunktet {{endpoint}} er ikkje implementert for {{serverType}}",
|
||||
"genericError": "Ein feil skjedde",
|
||||
"invalidJson": "Ugyldig JSON",
|
||||
"invalidServer": "Ugyldig sørvar",
|
||||
"localFontAccessDenied": "Ingen tilgang til lokale skrifttypar",
|
||||
"loginRateError": "For mange innloggingsforsøk, venlegast prøv på nytt om nokon sekundar",
|
||||
"mpvRequired": "MPV nødvendig",
|
||||
"multipleServerSaveQueueError": "Spelekøen har ein eller fleire songar som ikkje er frå den noverande sørvaren. Dette er ikkje støtta",
|
||||
"networkError": "Ein nettverksfeil skjedde",
|
||||
"noNetwork": "Sørvar utilgjengeleg",
|
||||
"noNetworkDescription": "Kunne ikkje kople til denne sørvaren"
|
||||
}
|
||||
}
|
||||
+1031
-948
File diff suppressed because it is too large
Load Diff
+417
-406
File diff suppressed because it is too large
Load Diff
+699
-368
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"common": {
|
||||
"confirm": "confirmă",
|
||||
"create": "creează",
|
||||
"biography": "biografie",
|
||||
"areYouSure": "ești sigur?",
|
||||
"no": "nu",
|
||||
"name": "nume",
|
||||
"ok": "ok",
|
||||
"note": "notă",
|
||||
"yes": "da",
|
||||
"explicit": "explicit",
|
||||
"year": "an",
|
||||
"menu": "meniu"
|
||||
"confirm": "Confirmă",
|
||||
"create": "Creează",
|
||||
"biography": "Biografie",
|
||||
"areYouSure": "Ești sigur?",
|
||||
"no": "Nu",
|
||||
"name": "Nume",
|
||||
"ok": "Ok",
|
||||
"note": "Notă",
|
||||
"yes": "Da",
|
||||
"explicit": "Explicit",
|
||||
"year": "An",
|
||||
"menu": "Meniu"
|
||||
},
|
||||
"filter": {
|
||||
"biography": "biografie"
|
||||
"biography": "Biografie"
|
||||
}
|
||||
}
|
||||
|
||||
+963
-765
File diff suppressed because it is too large
Load Diff
+579
-579
File diff suppressed because it is too large
Load Diff
+438
-438
File diff suppressed because it is too large
Load Diff
+429
-429
File diff suppressed because it is too large
Load Diff
+341
-341
@@ -1,323 +1,323 @@
|
||||
{
|
||||
"action": {
|
||||
"editPlaylist": "redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "gå till sida",
|
||||
"moveToTop": "flytta till toppen",
|
||||
"clearQueue": "rensa kö",
|
||||
"addToFavorites": "lägg till $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "lägg till $t(entity.playlist, {\"count\": 1})",
|
||||
"createPlaylist": "skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromPlaylist": "ta bort från $t(entity.playlist, {\"count\": 1})",
|
||||
"viewPlaylists": "visa $t(entity.playlist, {\"count\": 2})",
|
||||
"editPlaylist": "Redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "Gå till sida",
|
||||
"moveToTop": "Flytta till toppen",
|
||||
"clearQueue": "Rensa kö",
|
||||
"addToFavorites": "Lägg till $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Lägg till $t(entity.playlist, {\"count\": 1})",
|
||||
"createPlaylist": "Skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromPlaylist": "Ta bort från $t(entity.playlist, {\"count\": 1})",
|
||||
"viewPlaylists": "Visa $t(entity.playlist, {\"count\": 2})",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"deletePlaylist": "ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "ta bort från kö",
|
||||
"deselectAll": "avmarkera alla",
|
||||
"moveToBottom": "flytta till botten",
|
||||
"setRating": "sätt betyg",
|
||||
"toggleSmartPlaylistEditor": "växla $t(entity.smartPlaylist) redigerare",
|
||||
"removeFromFavorites": "ta bort från $t(entity.favorite, {\"count\": 2})",
|
||||
"downloadStarted": "startade nedladdning av {{count}} objekt",
|
||||
"moveToNext": "flytta till nästa",
|
||||
"moveUp": "flytta upp",
|
||||
"moveDown": "flytta ner",
|
||||
"holdToMoveToTop": "håll för att flytta till toppen",
|
||||
"holdToMoveToBottom": "håll för att flytta till botten",
|
||||
"moveItems": "flytta objekt",
|
||||
"shuffle": "slumpa",
|
||||
"shuffleAll": "slumpa alla",
|
||||
"shuffleSelected": "slumpa valda",
|
||||
"viewMore": "visa mer",
|
||||
"deletePlaylist": "Ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "Ta bort från kö",
|
||||
"deselectAll": "Avmarkera alla",
|
||||
"moveToBottom": "Flytta till botten",
|
||||
"setRating": "Sätt betyg",
|
||||
"toggleSmartPlaylistEditor": "Växla $t(entity.smartPlaylist) redigerare",
|
||||
"removeFromFavorites": "Ta bort från $t(entity.favorite, {\"count\": 2})",
|
||||
"downloadStarted": "Startade nedladdning av {{count}} objekt",
|
||||
"moveToNext": "Flytta till nästa",
|
||||
"moveUp": "Flytta upp",
|
||||
"moveDown": "Flytta ner",
|
||||
"holdToMoveToTop": "Håll för att flytta till toppen",
|
||||
"holdToMoveToBottom": "Håll för att flytta till botten",
|
||||
"moveItems": "Flytta objekt",
|
||||
"shuffle": "Slumpa",
|
||||
"shuffleAll": "Slumpa alla",
|
||||
"shuffleSelected": "Slumpa valda",
|
||||
"viewMore": "Visa mer",
|
||||
"openIn": {
|
||||
"lastfm": "Öppna i Last.fm",
|
||||
"musicbrainz": "Öppna i MusicBrainz"
|
||||
},
|
||||
"createRadioStation": "skapa $t(entity.radioStation, {\"count\": 1})",
|
||||
"deleteRadioStation": "ta bort $t(entity.radioStation, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "lägg till eller ta bort från markerade",
|
||||
"selectRangeOfItems": "välj en mängd objekt",
|
||||
"selectAll": "markera alla",
|
||||
"openApplicationDirectory": "öppna applikationskatalog"
|
||||
"createRadioStation": "Skapa $t(entity.radioStation, {\"count\": 1})",
|
||||
"deleteRadioStation": "Ta bort $t(entity.radioStation, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "Lägg till eller ta bort från markerade",
|
||||
"selectRangeOfItems": "Välj en mängd objekt",
|
||||
"selectAll": "Markera alla",
|
||||
"openApplicationDirectory": "Öppna applikationskatalog"
|
||||
},
|
||||
"common": {
|
||||
"backward": "bakåt",
|
||||
"increase": "öka",
|
||||
"rating": "betyg",
|
||||
"bpm": "bpm",
|
||||
"refresh": "laddaom",
|
||||
"unknown": "okänd",
|
||||
"areYouSure": "är du säker?",
|
||||
"edit": "redigera",
|
||||
"favorite": "favorit",
|
||||
"left": "vänster",
|
||||
"save": "spara",
|
||||
"right": "höger",
|
||||
"currentSong": "aktuell $t(entity.track, {\"count\": 1})",
|
||||
"collapse": "kollaps",
|
||||
"trackNumber": "spår",
|
||||
"descending": "fallande",
|
||||
"add": "lägg till",
|
||||
"gap": "avstånd",
|
||||
"ascending": "stigande",
|
||||
"dismiss": "avskeda",
|
||||
"year": "år",
|
||||
"manage": "hantera",
|
||||
"limit": "gräns",
|
||||
"minimize": "minimera",
|
||||
"modified": "modifierad",
|
||||
"duration": "längd",
|
||||
"name": "namn",
|
||||
"maximize": "maximera",
|
||||
"decrease": "minska",
|
||||
"ok": "ok",
|
||||
"description": "beskrivning",
|
||||
"configure": "konfigurera",
|
||||
"path": "sökväg",
|
||||
"no": "nej",
|
||||
"owner": "ägare",
|
||||
"enable": "aktivera",
|
||||
"clear": "töm",
|
||||
"forward": "framåt",
|
||||
"delete": "ta bort",
|
||||
"cancel": "avbryt",
|
||||
"forceRestartRequired": "starta om för att tillämpa ändringar... Stäng meddelandet för att starta om",
|
||||
"setting_one": "inställning",
|
||||
"backward": "Bakåt",
|
||||
"increase": "Öka",
|
||||
"rating": "Betyg",
|
||||
"bpm": "Bpm",
|
||||
"refresh": "Laddaom",
|
||||
"unknown": "Okänd",
|
||||
"areYouSure": "Är du säker?",
|
||||
"edit": "Redigera",
|
||||
"favorite": "Favorit",
|
||||
"left": "Vänster",
|
||||
"save": "Spara",
|
||||
"right": "Höger",
|
||||
"currentSong": "Aktuell $t(entity.track, {\"count\": 1})",
|
||||
"collapse": "Kollaps",
|
||||
"trackNumber": "Spår",
|
||||
"descending": "Fallande",
|
||||
"add": "Lägg till",
|
||||
"gap": "Avstånd",
|
||||
"ascending": "Stigande",
|
||||
"dismiss": "Avskeda",
|
||||
"year": "År",
|
||||
"manage": "Hantera",
|
||||
"limit": "Gräns",
|
||||
"minimize": "Minimera",
|
||||
"modified": "Modifierad",
|
||||
"duration": "Längd",
|
||||
"name": "Namn",
|
||||
"maximize": "Maximera",
|
||||
"decrease": "Minska",
|
||||
"ok": "Ok",
|
||||
"description": "Beskrivning",
|
||||
"configure": "Konfigurera",
|
||||
"path": "Sökväg",
|
||||
"no": "Nej",
|
||||
"owner": "Ägare",
|
||||
"enable": "Aktivera",
|
||||
"clear": "Töm",
|
||||
"forward": "Framåt",
|
||||
"delete": "Ta bort",
|
||||
"cancel": "Avbryt",
|
||||
"forceRestartRequired": "Starta om för att tillämpa ändringar... Stäng meddelandet för att starta om",
|
||||
"setting_one": "Inställning",
|
||||
"setting_other": "",
|
||||
"version": "version",
|
||||
"title": "titel",
|
||||
"filter_one": "filter",
|
||||
"filter_other": "filter",
|
||||
"filters": "filter",
|
||||
"create": "skapa",
|
||||
"bitrate": "bithastighet",
|
||||
"saveAndReplace": "spara och skrivöver",
|
||||
"action_one": "handling",
|
||||
"action_other": "handlingar",
|
||||
"playerMustBePaused": "spelaren måste pausas",
|
||||
"confirm": "bekräfta",
|
||||
"resetToDefault": "återställ till standard",
|
||||
"home": "hem",
|
||||
"comingSoon": "kommer snart…",
|
||||
"reset": "nollställ",
|
||||
"channel_one": "kanal",
|
||||
"channel_other": "kanaler",
|
||||
"disable": "inaktivera",
|
||||
"sortOrder": "ordning",
|
||||
"none": "ingen",
|
||||
"menu": "meny",
|
||||
"restartRequired": "omstart krävs",
|
||||
"previousSong": "föregående $t(entity.track, {\"count\": 1})",
|
||||
"noResultsFromQuery": "frågan returnerade inga resultat",
|
||||
"quit": "avsluta",
|
||||
"expand": "expandera",
|
||||
"search": "sök",
|
||||
"saveAs": "spara som",
|
||||
"disc": "skiva",
|
||||
"yes": "ja",
|
||||
"random": "slumpmässig",
|
||||
"size": "storlek",
|
||||
"biography": "biografi",
|
||||
"note": "anteckning",
|
||||
"center": "center",
|
||||
"explicitStatus": "olämplig status",
|
||||
"additionalParticipants": "ytterligare medverkare",
|
||||
"newVersion": "en ny version har installerats {{version}}",
|
||||
"viewReleaseNotes": "se utgåveinformation",
|
||||
"bitDepth": "bitdjup",
|
||||
"close": "stäng",
|
||||
"codec": "kodek",
|
||||
"doNotShowAgain": "visa inte detta igen",
|
||||
"view": "visa",
|
||||
"externalLinks": "externa länkar",
|
||||
"faster": "snabbare",
|
||||
"version": "Version",
|
||||
"title": "Titel",
|
||||
"filter_one": "Filter",
|
||||
"filter_other": "Filter",
|
||||
"filters": "Filter",
|
||||
"create": "Skapa",
|
||||
"bitrate": "Bithastighet",
|
||||
"saveAndReplace": "Spara och skrivöver",
|
||||
"action_one": "Handling",
|
||||
"action_other": "Handlingar",
|
||||
"playerMustBePaused": "Spelaren måste pausas",
|
||||
"confirm": "Bekräfta",
|
||||
"resetToDefault": "Återställ till standard",
|
||||
"home": "Hem",
|
||||
"comingSoon": "Kommer snart…",
|
||||
"reset": "Nollställ",
|
||||
"channel_one": "Kanal",
|
||||
"channel_other": "Kanaler",
|
||||
"disable": "Inaktivera",
|
||||
"sortOrder": "Ordning",
|
||||
"none": "Ingen",
|
||||
"menu": "Meny",
|
||||
"restartRequired": "Omstart krävs",
|
||||
"previousSong": "Föregående $t(entity.track, {\"count\": 1})",
|
||||
"noResultsFromQuery": "Frågan returnerade inga resultat",
|
||||
"quit": "Avsluta",
|
||||
"expand": "Expandera",
|
||||
"search": "Sök",
|
||||
"saveAs": "Spara som",
|
||||
"disc": "Skiva",
|
||||
"yes": "Ja",
|
||||
"random": "Slumpmässig",
|
||||
"size": "Storlek",
|
||||
"biography": "Biografi",
|
||||
"note": "Anteckning",
|
||||
"center": "Center",
|
||||
"explicitStatus": "Olämplig status",
|
||||
"additionalParticipants": "Ytterligare medverkare",
|
||||
"newVersion": "En ny version har installerats {{version}}",
|
||||
"viewReleaseNotes": "Se utgåveinformation",
|
||||
"bitDepth": "Bitdjup",
|
||||
"close": "Stäng",
|
||||
"codec": "Kodek",
|
||||
"doNotShowAgain": "Visa inte detta igen",
|
||||
"view": "Visa",
|
||||
"externalLinks": "Externa länkar",
|
||||
"faster": "Snabbare",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"noFilters": "inga filter konfigurerade",
|
||||
"preview": "förhandsvisa",
|
||||
"private": "privat",
|
||||
"public": "allmän",
|
||||
"recordLabel": "skivbolag",
|
||||
"releaseType": "utgåvetyp",
|
||||
"reload": "ladda om",
|
||||
"sampleRate": "samplingstakt",
|
||||
"slower": "långsammare",
|
||||
"share": "dela",
|
||||
"sort": "sortera",
|
||||
"tags": "taggar",
|
||||
"translation": "översättning",
|
||||
"explicit": "olämplig",
|
||||
"clean": "städad",
|
||||
"gridRows": "rutnätsrader",
|
||||
"tableColumns": "tabellkolumner",
|
||||
"noFilters": "Inga filter konfigurerade",
|
||||
"preview": "Förhandsvisa",
|
||||
"private": "Privat",
|
||||
"public": "Allmän",
|
||||
"recordLabel": "Skivbolag",
|
||||
"releaseType": "Utgåvetyp",
|
||||
"reload": "Ladda om",
|
||||
"sampleRate": "Samplingstakt",
|
||||
"slower": "Långsammare",
|
||||
"share": "Dela",
|
||||
"sort": "Sortera",
|
||||
"tags": "Taggar",
|
||||
"translation": "Översättning",
|
||||
"explicit": "Olämplig",
|
||||
"clean": "Städad",
|
||||
"gridRows": "Rutnätsrader",
|
||||
"tableColumns": "Tabellkolumner",
|
||||
"itemsMore": "{{count}} fler",
|
||||
"countSelected": "{{count}} markerade"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "starta om servern för att tillämpa den nya porten",
|
||||
"systemFontError": "ett fel uppstod vid försök att hämta systemteckensnitt",
|
||||
"playbackError": "ett fel uppstod vid försök att spela upp media",
|
||||
"endpointNotImplementedError": "endpoint {{endpoint}} är inte implementerad för {{serverType}}",
|
||||
"remotePortError": "ett fel uppstod vid försök att ange serverporten",
|
||||
"serverRequired": "server krävs",
|
||||
"authenticationFailed": "autentiseringen misslyckades",
|
||||
"apiRouteError": "det går inte att dirigera begäran",
|
||||
"genericError": "ett fel uppstod",
|
||||
"credentialsRequired": "autentiseringsuppgifter som krävs",
|
||||
"sessionExpiredError": "din session har löpt ut",
|
||||
"remotePortWarning": "Starta om servern för att tillämpa den nya porten",
|
||||
"systemFontError": "Ett fel uppstod vid försök att hämta systemteckensnitt",
|
||||
"playbackError": "Ett fel uppstod vid försök att spela upp media",
|
||||
"endpointNotImplementedError": "Endpoint {{endpoint}} är inte implementerad för {{serverType}}",
|
||||
"remotePortError": "Ett fel uppstod vid försök att ange serverporten",
|
||||
"serverRequired": "Server krävs",
|
||||
"authenticationFailed": "Autentiseringen misslyckades",
|
||||
"apiRouteError": "Det går inte att dirigera begäran",
|
||||
"genericError": "Ett fel uppstod",
|
||||
"credentialsRequired": "Autentiseringsuppgifter som krävs",
|
||||
"sessionExpiredError": "Din session har löpt ut",
|
||||
"remoteEnableError": "Ett fel uppstod vid försök att $t(common.enable) servern",
|
||||
"localFontAccessDenied": "åtkomst nekad till lokala teckensnitt",
|
||||
"serverNotSelectedError": "ingen server vald",
|
||||
"remoteDisableError": "ett fel uppstod vid försök av $t(common.disable) servern",
|
||||
"localFontAccessDenied": "Åtkomst nekad till lokala teckensnitt",
|
||||
"serverNotSelectedError": "Ingen server vald",
|
||||
"remoteDisableError": "Ett fel uppstod vid försök av $t(common.disable) servern",
|
||||
"mpvRequired": "MPV krävs",
|
||||
"audioDeviceFetchError": "ett fel uppstod vid hämtning av ljudenheter",
|
||||
"invalidServer": "ogiltig server",
|
||||
"loginRateError": "för många inloggningsförsök, försök igen om några sekunder",
|
||||
"badAlbum": "du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
||||
"badValue": "felaktigt alternativ \"{{value}}\". detta värde existerar inte längre",
|
||||
"multipleServerSaveQueueError": "spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
|
||||
"networkError": "en nätverksfel uppstod",
|
||||
"notificationDenied": "åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
|
||||
"openError": "kunde inte öppna filen",
|
||||
"settingsSyncError": "diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
|
||||
"audioDeviceFetchError": "Ett fel uppstod vid hämtning av ljudenheter",
|
||||
"invalidServer": "Ogiltig server",
|
||||
"loginRateError": "För många inloggningsförsök, försök igen om några sekunder",
|
||||
"badAlbum": "Du ser denna sidan eftersom denna låten inte är en del av ett album. du ser troligtvis detta problemet för att du har en låt på toppnivån i din musikmapp. Jellyfin grupperar bara låtar om de finns i en mapp",
|
||||
"badValue": "Felaktigt alternativ \"{{value}}\". detta värde existerar inte längre",
|
||||
"multipleServerSaveQueueError": "Spelningskön har en eller flera låtar som inte är från den nuvarande valda servern. detta är inte stöttat",
|
||||
"networkError": "En nätverksfel uppstod",
|
||||
"notificationDenied": "Åtkomst till notifieringarna var nekad. inställningen har ingen verkan",
|
||||
"openError": "Kunde inte öppna filen",
|
||||
"settingsSyncError": "Diskrepans hittades mellan inställningarna för renderingsprocessen och huvudprocessen. starta om applikationen för att ändringarna ska tillämpas"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "mest spelade",
|
||||
"comment": "kommentar",
|
||||
"playCount": "antal spelningar",
|
||||
"recentlyUpdated": "nyligen uppdaterad",
|
||||
"mostPlayed": "Mest spelade",
|
||||
"comment": "Kommentar",
|
||||
"playCount": "Antal spelningar",
|
||||
"recentlyUpdated": "Nyligen uppdaterad",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"isCompilation": "är kompilering",
|
||||
"recentlyPlayed": "nyligen spelad",
|
||||
"isRated": "är betygsatt",
|
||||
"isCompilation": "Är kompilering",
|
||||
"recentlyPlayed": "Nyligen spelad",
|
||||
"isRated": "Är betygsatt",
|
||||
"owner": "$t(common.owner)",
|
||||
"title": "titel",
|
||||
"rating": "betyg",
|
||||
"search": "sök",
|
||||
"bitrate": "bithastighet",
|
||||
"title": "Titel",
|
||||
"rating": "Betyg",
|
||||
"search": "Sök",
|
||||
"bitrate": "Bithastighet",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"recentlyAdded": "nyligen tillagda",
|
||||
"note": "anteckning",
|
||||
"name": "namn",
|
||||
"dateAdded": "datum tillagt",
|
||||
"releaseDate": "utgivningsdag",
|
||||
"communityRating": "betyg från communityn",
|
||||
"path": "sökväg",
|
||||
"favorited": "favoritmärkt",
|
||||
"recentlyAdded": "Nyligen tillagda",
|
||||
"note": "Anteckning",
|
||||
"name": "Namn",
|
||||
"dateAdded": "Datum tillagt",
|
||||
"releaseDate": "Utgivningsdag",
|
||||
"communityRating": "Betyg från communityn",
|
||||
"path": "Sökväg",
|
||||
"favorited": "Favoritmärkt",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"isRecentlyPlayed": "spelas nyligen",
|
||||
"isFavorited": "är favoritmärkt",
|
||||
"bpm": "bpm",
|
||||
"releaseYear": "utgivningsår",
|
||||
"id": "id",
|
||||
"disc": "skiva",
|
||||
"biography": "biografi",
|
||||
"isRecentlyPlayed": "Spelas nyligen",
|
||||
"isFavorited": "Är favoritmärkt",
|
||||
"bpm": "Bpm",
|
||||
"releaseYear": "Utgivningsår",
|
||||
"id": "Id",
|
||||
"disc": "Skiva",
|
||||
"biography": "Biografi",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"duration": "längd",
|
||||
"isPublic": "är offentlig",
|
||||
"random": "slumpmässig",
|
||||
"lastPlayed": "senast spelad",
|
||||
"toYear": "till år",
|
||||
"fromYear": "från år",
|
||||
"duration": "Längd",
|
||||
"isPublic": "Är offentlig",
|
||||
"random": "Slumpmässig",
|
||||
"lastPlayed": "Senast spelad",
|
||||
"toYear": "Till år",
|
||||
"fromYear": "Från år",
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"trackNumber": "spår",
|
||||
"songCount": "sångräkning",
|
||||
"criticRating": "kritikerbetyg",
|
||||
"trackNumber": "Spår",
|
||||
"songCount": "Sångräkning",
|
||||
"criticRating": "Kritikerbetyg",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2}) antal",
|
||||
"explicitStatus": "$t(common.explicitStatus)"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
"title": "ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"title": "Ta bort $t(entity.playlist, {\"count\": 1})",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) har tagits bort",
|
||||
"input_confirm": "Skriv namnet på $t(entity.playlist, {\"count\": 1}) för att bekräfta"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
"title": "skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"input_public": "offentlig",
|
||||
"title": "Skapa $t(entity.playlist, {\"count\": 1})",
|
||||
"input_public": "Offentlig",
|
||||
"input_name": "$t(common.name)",
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) skapad",
|
||||
"input_owner": "$t(common.owner)"
|
||||
},
|
||||
"addServer": {
|
||||
"title": "lägg till server",
|
||||
"input_username": "användarnamn",
|
||||
"input_url": "länk",
|
||||
"input_password": "lösenord",
|
||||
"input_legacyAuthentication": "aktivera äldre autentisering",
|
||||
"input_name": "server namn",
|
||||
"success": "servern har lagts till",
|
||||
"input_savePassword": "spara lösenord",
|
||||
"ignoreSsl": "ignorera ssl ($t(common.restartRequired))",
|
||||
"ignoreCors": "ignorera cors ($t(common.restartRequired))",
|
||||
"error_savePassword": "ett fel uppstod när lösenordet skulle sparas",
|
||||
"input_preferInstantMix": "föredra instant mixning",
|
||||
"input_preferInstantMixDescription": "använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
|
||||
"title": "Lägg till server",
|
||||
"input_username": "Användarnamn",
|
||||
"input_url": "Länk",
|
||||
"input_password": "Lösenord",
|
||||
"input_legacyAuthentication": "Aktivera äldre autentisering",
|
||||
"input_name": "Server namn",
|
||||
"success": "Servern har lagts till",
|
||||
"input_savePassword": "Spara lösenord",
|
||||
"ignoreSsl": "Ignorera ssl ($t(common.restartRequired))",
|
||||
"ignoreCors": "Ignorera cors ($t(common.restartRequired))",
|
||||
"error_savePassword": "Ett fel uppstod när lösenordet skulle sparas",
|
||||
"input_preferInstantMix": "Föredra instant mixning",
|
||||
"input_preferInstantMixDescription": "Använd bara instant mixning för att få liknande låtar. användbar om du har plugin för att förändra detta beteendet"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "lägg till i $t(entity.playlist, {\"count\": 1})",
|
||||
"input_skipDuplicates": "hoppa över dubbletter",
|
||||
"success": "Lade till $t(entity.trackWithCount, {\"count\": {{message}} }) till $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "Lägg till i $t(entity.playlist, {\"count\": 1})",
|
||||
"input_skipDuplicates": "Hoppa över dubbletter",
|
||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||
"create": "skapa $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"searchOrCreate": "sök $t(entity.playlist, {\"count\": 2}) eller skriv för att skapa en ny"
|
||||
"create": "Skapa $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"searchOrCreate": "Sök $t(entity.playlist, {\"count\": 2}) eller skriv för att skapa en ny"
|
||||
},
|
||||
"updateServer": {
|
||||
"title": "uppdatera server",
|
||||
"success": "servern har uppdaterats"
|
||||
"title": "Uppdatera server",
|
||||
"success": "Servern har uppdaterats"
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "matcha alla",
|
||||
"input_optionMatchAny": "matcha något"
|
||||
"input_optionMatchAll": "Matcha alla",
|
||||
"input_optionMatchAny": "Matcha något"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
"input_artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"title": "sångtext sök"
|
||||
"title": "Sångtext sök"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"title": "Redigera $t(entity.playlist, {\"count\": 1})",
|
||||
"publicJellyfinNote": "Jellyfin visar av någon anledning inte om en spellista är publik eller inte. Om du önskar att denna ska förbli publik, så får du ha följande indata markerade"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
"title": "lägg till objekt till kön",
|
||||
"title": "Lägg till objekt till kön",
|
||||
"description": "Åtgärden kommer att lägga till alla objekt till den nuvarande filtrerade vyn"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "radiostation skapades",
|
||||
"title": "skapa radiostation",
|
||||
"input_homepageUrl": "hemside-URL",
|
||||
"input_name": "namn",
|
||||
"input_streamUrl": "stream url"
|
||||
"success": "Radiostation skapades",
|
||||
"title": "Skapa radiostation",
|
||||
"input_homepageUrl": "Hemside-URL",
|
||||
"input_name": "Namn",
|
||||
"input_streamUrl": "Stream url"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
"showLyricMatch": "Visa låttext matchning",
|
||||
"dynamicBackground": "dynamisk bakgrund",
|
||||
"followCurrentLyric": "följ aktuell låttext",
|
||||
"opacity": "ogenomskinlighet",
|
||||
"lyricSize": "låttext storlek",
|
||||
"lyricAlignment": "låttext justering",
|
||||
"lyricGap": "låttext mellanrum",
|
||||
"synchronized": "synkroniserad",
|
||||
"showLyricProvider": "visa sångtextleverantör",
|
||||
"unsynchronized": "osynkroniserad"
|
||||
"dynamicBackground": "Dynamisk bakgrund",
|
||||
"followCurrentLyric": "Följ aktuell låttext",
|
||||
"opacity": "Ogenomskinlighet",
|
||||
"lyricSize": "Låttext storlek",
|
||||
"lyricAlignment": "Låttext justering",
|
||||
"lyricGap": "Låttext mellanrum",
|
||||
"synchronized": "Synkroniserad",
|
||||
"showLyricProvider": "Visa sångtextleverantör",
|
||||
"unsynchronized": "Osynkroniserad"
|
||||
},
|
||||
"lyrics": "sångtext",
|
||||
"related": "relaterad"
|
||||
"lyrics": "Sångtext",
|
||||
"related": "Relaterad"
|
||||
},
|
||||
"appMenu": {
|
||||
"selectServer": "välj server",
|
||||
"version": "version {{version}}",
|
||||
"selectServer": "Välj server",
|
||||
"version": "Version {{version}}",
|
||||
"settings": "$t(common.setting, {\"count\": 2})",
|
||||
"manageServers": "hantera servrar",
|
||||
"expandSidebar": "expandera sidofältet",
|
||||
"openBrowserDevtools": "öppna webbläsarens utvecklingsverktyg",
|
||||
"manageServers": "Hantera servrar",
|
||||
"expandSidebar": "Expandera sidofältet",
|
||||
"openBrowserDevtools": "Öppna webbläsarens utvecklingsverktyg",
|
||||
"quit": "$t(common.quit)",
|
||||
"goBack": "gå tillbaka",
|
||||
"goForward": "gå framåt",
|
||||
"collapseSidebar": "växla sidofältet"
|
||||
"goBack": "Gå tillbaka",
|
||||
"goForward": "Gå framåt",
|
||||
"collapseSidebar": "Växla sidofältet"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
@@ -336,20 +336,20 @@
|
||||
"play": "$t(player.play)",
|
||||
"numberSelected": "{{count}} vald",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"download": "ladda ner",
|
||||
"download": "Ladda ner",
|
||||
"moveItems": "$t(action.moveItems)",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "dela objekt",
|
||||
"goTo": "gå till",
|
||||
"goToAlbum": "gå till $t(entity.album, {\"count\": 1})",
|
||||
"goToAlbumArtist": "gå till $t(entity.albumArtist, {\"count\": 1})",
|
||||
"showDetails": "hämta information"
|
||||
"shareItem": "Dela objekt",
|
||||
"goTo": "Gå till",
|
||||
"goToAlbum": "Gå till $t(entity.album, {\"count\": 1})",
|
||||
"goToAlbumArtist": "Gå till $t(entity.albumArtist, {\"count\": 1})",
|
||||
"showDetails": "Hämta information"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "mer från $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "mer från {{item}}"
|
||||
"moreFromArtist": "Mer från $t(entity.artist, {\"count\": 1})",
|
||||
"moreFromGeneric": "Mer från {{item}}"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||
@@ -358,124 +358,124 @@
|
||||
"title": "$t(entity.album, {\"count\": 2})"
|
||||
},
|
||||
"sidebar": {
|
||||
"nowPlaying": "nu spelas"
|
||||
"nowPlaying": "Nu spelas"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "mest spelade",
|
||||
"newlyAdded": "nytillkomna utgåvor",
|
||||
"explore": "utforska från ditt bibliotek",
|
||||
"recentlyPlayed": "nyligen spelat"
|
||||
"mostPlayed": "Mest spelade",
|
||||
"newlyAdded": "Nytillkomna utgåvor",
|
||||
"explore": "Utforska från ditt bibliotek",
|
||||
"recentlyPlayed": "Nyligen spelat"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "uppspelning",
|
||||
"generalTab": "allmänt",
|
||||
"hotkeysTab": "snabbtangenter",
|
||||
"windowTab": "fönster"
|
||||
"playbackTab": "Uppspelning",
|
||||
"generalTab": "Allmänt",
|
||||
"hotkeysTab": "Snabbtangenter",
|
||||
"windowTab": "Fönster"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
"serverCommands": "serverkommandon",
|
||||
"goToPage": "gå till sidan",
|
||||
"searchFor": "sök efter {{query}}"
|
||||
"serverCommands": "Serverkommandon",
|
||||
"goToPage": "Gå till sidan",
|
||||
"searchFor": "Sök efter {{query}}"
|
||||
},
|
||||
"title": "kommandon"
|
||||
"title": "Kommandon"
|
||||
},
|
||||
"manageServers": {
|
||||
"url": "URL",
|
||||
"username": "användarnamn",
|
||||
"editServerDetailsTooltip": "redigera serverinställningar",
|
||||
"removeServer": "ta bort server"
|
||||
"username": "Användarnamn",
|
||||
"editServerDetailsTooltip": "Redigera serverinställningar",
|
||||
"removeServer": "Ta bort server"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"playlist_one": "spellista",
|
||||
"playlist_other": "spellistor",
|
||||
"artist_one": "artist",
|
||||
"artist_other": "artister",
|
||||
"albumArtist_one": "albumartist",
|
||||
"albumArtist_other": "albumartister",
|
||||
"albumArtistCount_one": "{{count}} Albumartist",
|
||||
"albumArtistCount_other": "{{count}} Albumartister",
|
||||
"playlist_one": "Spellista",
|
||||
"playlist_other": "Spellistor",
|
||||
"artist_one": "Artist",
|
||||
"artist_other": "Artister",
|
||||
"albumArtist_one": "Albumartist",
|
||||
"albumArtist_other": "Albumartister",
|
||||
"albumArtistCount_one": "{{count}} albumartist",
|
||||
"albumArtistCount_other": "{{count}} albumartister",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_other": "{{count}} album",
|
||||
"favorite_one": "favorit",
|
||||
"favorite_other": "favoriter",
|
||||
"folder_one": "mapp",
|
||||
"folder_other": "mappar",
|
||||
"album_one": "album",
|
||||
"album_other": "album",
|
||||
"favorite_one": "Favorit",
|
||||
"favorite_other": "Favoriter",
|
||||
"folder_one": "Mapp",
|
||||
"folder_other": "Mappar",
|
||||
"album_one": "Album",
|
||||
"album_other": "Album",
|
||||
"playlistWithCount_one": "{{count}} spellista",
|
||||
"playlistWithCount_other": "{{count}} spellistor",
|
||||
"folderWithCount_one": "{{count}} mapp",
|
||||
"folderWithCount_other": "{{count}} mappar",
|
||||
"track_one": "spår",
|
||||
"track_other": "spår",
|
||||
"track_one": "Spår",
|
||||
"track_other": "Spår",
|
||||
"trackWithCount_one": "{{count}} spår",
|
||||
"trackWithCount_other": "{{count}} spår",
|
||||
"artistWithCount_one": "{{count}} artist",
|
||||
"artistWithCount_other": "{{count}} artister",
|
||||
"genre_one": "genre",
|
||||
"genre_other": "genrer",
|
||||
"genre_one": "Genre",
|
||||
"genre_other": "Genrer",
|
||||
"genreWithCount_one": "{{count}} genre",
|
||||
"genreWithCount_other": "{{count}} genrer",
|
||||
"play_one": "{{count}} spelning",
|
||||
"play_other": "{{count}} spelningar",
|
||||
"smartPlaylist": "smart $t(entity.playlist, {\"count\": 1})",
|
||||
"song_one": "låt",
|
||||
"song_other": "låtar",
|
||||
"radioStation_one": "radiostation",
|
||||
"radioStation_other": "radiostationer",
|
||||
"smartPlaylist": "Smart $t(entity.playlist, {\"count\": 1})",
|
||||
"song_one": "Låt",
|
||||
"song_other": "Låtar",
|
||||
"radioStation_one": "Radiostation",
|
||||
"radioStation_other": "Radiostationer",
|
||||
"radioStationWithCount_one": "{{count}} radiostation",
|
||||
"radioStationWithCount_other": "{{count}} radiostationer"
|
||||
},
|
||||
"player": {
|
||||
"repeat_all": "repetera alla",
|
||||
"repeat": "repetera",
|
||||
"queue_remove": "ta bort markerad",
|
||||
"playRandom": "spela slumpmässigt",
|
||||
"previous": "föregående",
|
||||
"favorite": "favorit",
|
||||
"next": "nästa",
|
||||
"shuffle": "blanda",
|
||||
"playbackFetchNoResults": "inga låtar hittades",
|
||||
"playbackFetchInProgress": "laddar låtar…",
|
||||
"addNext": "lägg till nästa",
|
||||
"playbackSpeed": "uppspelningshastighet",
|
||||
"playbackFetchCancel": "det här tar ett tag... stäng aviseringen för att avbryta",
|
||||
"play": "spela",
|
||||
"repeat_off": "repetera inaktiverad",
|
||||
"queue_clear": "rensa kö",
|
||||
"muted": "mutad",
|
||||
"queue_moveToTop": "flytta markerad till botten",
|
||||
"queue_moveToBottom": "flytta markerad till toppen",
|
||||
"addLast": "lägg till sist",
|
||||
"mute": "muta"
|
||||
"repeat_all": "Repetera alla",
|
||||
"repeat": "Repetera",
|
||||
"queue_remove": "Ta bort markerad",
|
||||
"playRandom": "Spela slumpmässigt",
|
||||
"previous": "Föregående",
|
||||
"favorite": "Favorit",
|
||||
"next": "Nästa",
|
||||
"shuffle": "Blanda",
|
||||
"playbackFetchNoResults": "Inga låtar hittades",
|
||||
"playbackFetchInProgress": "Laddar låtar…",
|
||||
"addNext": "Lägg till nästa",
|
||||
"playbackSpeed": "Uppspelningshastighet",
|
||||
"playbackFetchCancel": "Det här tar ett tag... stäng aviseringen för att avbryta",
|
||||
"play": "Spela",
|
||||
"repeat_off": "Repetera inaktiverad",
|
||||
"queue_clear": "Rensa kö",
|
||||
"muted": "Mutad",
|
||||
"queue_moveToTop": "Flytta markerad till toppen",
|
||||
"queue_moveToBottom": "Flytta markerad till botten",
|
||||
"addLast": "Lägg till sist",
|
||||
"mute": "Muta"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "min",
|
||||
"secondShort": "sek",
|
||||
"hourShort": "h",
|
||||
"dayShort": "dag"
|
||||
"minuteShort": "Min",
|
||||
"secondShort": "Sek",
|
||||
"hourShort": "H",
|
||||
"dayShort": "Dag"
|
||||
},
|
||||
"filterOperator": {
|
||||
"after": "är efter",
|
||||
"afterDate": "är efter (datum)",
|
||||
"before": "är före",
|
||||
"beforeDate": "är före (datum)",
|
||||
"contains": "innehåller",
|
||||
"endsWith": "slutar med",
|
||||
"inPlaylist": "är inom",
|
||||
"inTheLast": "är i den sista",
|
||||
"inTheRange": "är i spannet",
|
||||
"inTheRangeDate": "är i spannet (datum)",
|
||||
"is": "är",
|
||||
"isNot": "är inte",
|
||||
"isGreaterThan": "är större än",
|
||||
"isLessThan": "är mindre än",
|
||||
"matchesRegex": "matchar regex",
|
||||
"notContains": "innehåller inte",
|
||||
"notInPlaylist": "är inte inom",
|
||||
"notInTheLast": "är inte inom den sista",
|
||||
"startsWith": "startar med"
|
||||
"after": "Är efter",
|
||||
"afterDate": "Är efter (datum)",
|
||||
"before": "Är före",
|
||||
"beforeDate": "Är före (datum)",
|
||||
"contains": "Innehåller",
|
||||
"endsWith": "Slutar med",
|
||||
"inPlaylist": "Är inom",
|
||||
"inTheLast": "Är i den sista",
|
||||
"inTheRange": "Är i spannet",
|
||||
"inTheRangeDate": "Är i spannet (datum)",
|
||||
"is": "Är",
|
||||
"isNot": "Är inte",
|
||||
"isGreaterThan": "Är större än",
|
||||
"isLessThan": "Är mindre än",
|
||||
"matchesRegex": "Matchar regex",
|
||||
"notContains": "Innehåller inte",
|
||||
"notInPlaylist": "Är inte inom",
|
||||
"notInTheLast": "Är inte inom den sista",
|
||||
"startsWith": "Startar med"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,8 +627,8 @@
|
||||
"playbackFetchNoResults": "பாடல்கள் எதுவும் கிடைக்கவில்லை",
|
||||
"playbackSpeed": "பிளேபேக் விரைவு",
|
||||
"playRandom": "சீரற்ற முறையில் விளையாடுங்கள்",
|
||||
"queue_moveToBottom": "மேலே தேர்ந்தெடுக்கப்பட்ட நகர்த்து",
|
||||
"queue_moveToTop": "தேர்ந்தெடுக்கப்பட்டதை கீழே நகர்த்தவும்",
|
||||
"queue_moveToBottom": "தேர்ந்தெடுக்கப்பட்டதை கீழே நகர்த்தவும்",
|
||||
"queue_moveToTop": "மேலே தேர்ந்தெடுக்கப்பட்ட நகர்த்து",
|
||||
"skip_back": "பின்னோக்கி தவிர்க்கவும்",
|
||||
"skip_forward": "முன்னோக்கி தவிர்க்கவும்",
|
||||
"stop": "நிறுத்து",
|
||||
@@ -800,7 +800,7 @@
|
||||
"enableRemote": "ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்கவும்",
|
||||
"enableRemote_description": "பயன்பாட்டைக் கட்டுப்படுத்த மற்ற சாதனங்களை அனுமதிக்க ரிமோட் கண்ட்ரோல் சேவையகத்தை இயக்குகிறது",
|
||||
"externalLinks": "வெளிப்புற இணைப்புகளைக் காட்டு",
|
||||
"externalLinks_description": "கலைஞர்/ஆல்பம் பக்கங்களில் வெளிப்புற இணைப்புகளை (last.fm, மியூசிக் ப்ரெய்ன்ச்) காண்பிக்க உதவுகிறது",
|
||||
"externalLinks_description": "கலைஞர்/ஆல்பம் பக்கங்களில் வெளிப்புற இணைப்புகளை (Last.fm, மியூசிக் ப்ரெய்ன்ச்) காண்பிக்க உதவுகிறது",
|
||||
"exitToTray": "தட்டில் வெளியேறவும்",
|
||||
"globalMediaHotkeys": "உலகளாவிய மீடியா ஆட்கீச்",
|
||||
"discordUpdateInterval": "{{discord}} பணக்கார இருப்பு புதுப்பிப்பு இடைவெளி",
|
||||
@@ -872,7 +872,7 @@
|
||||
"discordServeImage_description": "சேவையகத்திலிருந்தே {{discord}} சிறந்த இருப்புக்கான கவர் ஆர்ட்டைப் பகிரவும், செல்லிஃபின் மற்றும் நவிட்ரோமுக்கு மட்டுமே கிடைக்கும். படங்களைப் பெற {{discord}} ஒரு போட்டைப் பயன்படுத்துகிறது, எனவே உங்கள் சர்வர் பொது இணையத்திலிருந்து அணுகக்கூடியதாக இருக்க வேண்டும்",
|
||||
"preferLocalLyrics": "உள்ளக பாடல்களை விரும்புங்கள்",
|
||||
"preferLocalLyrics_description": "கிடைக்கும்போது தொலைநிலை பாடல்களை விட உள்ளக பாடல்களை விரும்புங்கள்",
|
||||
"lastfm": "last.fm இணைப்புகளைக் காட்டு",
|
||||
"lastfm": "Last.fm இணைப்புகளைக் காட்டு",
|
||||
"lastfm_description": "கலைஞர்/ஆல்பம் பக்கங்களில் Last.fm க்கான இணைப்புகளைக் காட்டு",
|
||||
"musicbrainz": "மியூசிக் பிரேன்ச் இணைப்புகளைக் காட்டு",
|
||||
"musicbrainz_description": "கலைஞர்/ஆல்பம் பக்கங்களில் மியூசிக் பிரைன்ச் இணைப்புகளைக் காட்டு, அங்கு மியூசிக் பிரைன்ச் ID உள்ளது",
|
||||
@@ -881,7 +881,6 @@
|
||||
"preservePitch": "சுருதியைப் பாதுகாக்கவும்",
|
||||
"preservePitch_description": "பின்னணி வேகத்தை மாற்றும்போது சுருதியைப் பாதுகாக்கிறது",
|
||||
"autoDJ": "ஆட்டோ டி.சே",
|
||||
"autoDJ_description": "தானாக வரிசையில் ஒத்த பாடல்களைச் சேர்க்கவும்",
|
||||
"autoDJ_itemCount": "பொருள் எண்ணிக்கை",
|
||||
"autoDJ_itemCount_description": "ஆட்டோ DJ இயக்கப்பட்டிருக்கும் போது, வரிசையில் சேர்க்க முயற்சிக்கும் உருப்படிகளின் எண்ணிக்கை",
|
||||
"autoDJ_timing": "நேரவிவரம்",
|
||||
@@ -925,7 +924,7 @@
|
||||
"exportImportSettings_control_title": "இறக்குமதி / ஏற்றுமதி அமைப்புகள்",
|
||||
"exportImportSettings_destructiveWarning": "அமைப்புகளை இறக்குமதி செய்வது அழிவுகரமானது, கீழே உள்ள \"இறக்குமதி\" என்பதைக் சொடுக்கு செய்வதற்கு முன் மேலே உள்ளவற்றை மதிப்பாய்வு செய்யவும்!",
|
||||
"exportImportSettings_importBtn": "இறக்குமதி அமைப்புகள்",
|
||||
"exportImportSettings_importModalTitle": "feishin அமைப்புகளை இறக்குமதி செய்யவும்",
|
||||
"exportImportSettings_importModalTitle": "Feishin அமைப்புகளை இறக்குமதி செய்யவும்",
|
||||
"exportImportSettings_importSuccess": "அமைப்புகள் வெற்றிகரமாக இறக்குமதி செய்யப்பட்டன!",
|
||||
"exportImportSettings_notValidJSON": "அனுப்பப்பட்ட கோப்பு சாதொபொகு செல்லுபடியாகாது",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" தவறானது - {{reason}}",
|
||||
@@ -948,8 +947,8 @@
|
||||
"logLevel_optionError": "பிழை",
|
||||
"logLevel_optionInfo": "தகவல்",
|
||||
"logLevel_optionWarn": "முன்னறிவிப்பு",
|
||||
"mpvExtraParameters": "mpv கூடுதல் அளவுருக்கள்",
|
||||
"mpvExtraParameters_description": "mpv க்கு அனுப்ப கூடுதல் வாதங்கள்",
|
||||
"mpvExtraParameters": "MPV கூடுதல் அளவுருக்கள்",
|
||||
"mpvExtraParameters_description": "MPV க்கு அனுப்ப கூடுதல் வாதங்கள்",
|
||||
"notify": "பாடல் அறிவிப்புகளை இயக்கவும்",
|
||||
"notify_description": "தற்போதைய பாடலை மாற்றும்போது அறிவிப்புகளைக் காட்டு",
|
||||
"pathReplace": "கோப்பு பாதை மாற்று",
|
||||
@@ -1015,7 +1014,7 @@
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "$t(common.biography)",
|
||||
"bitrate": "$t(common.bitrate)",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"bpm": "$t(common.BPM)",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"codec": "$t(common.codec)",
|
||||
"dateAdded": "தேதி சேர்க்கப்பட்டது",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "Idagdag sa $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Idagdag sa $t(entity.playlist, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "Idagdag o alisin sa pinili",
|
||||
"collapseAllFolders": "Isara lahat ng mga folder",
|
||||
"expandAllFolders": "Buksan lahat ng mga folder",
|
||||
"createPlaylist": "Gumawa $t(entity.playlist, {\"count\": 1})",
|
||||
"createRadioStation": "Gumawa $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "Piliin lahat",
|
||||
"deselectAll": "Huwag piliin lahat",
|
||||
"downloadStarted": "Nagsimulang mag-dowload ng {{count}} (mga) aytem"
|
||||
}
|
||||
}
|
||||
+746
-570
File diff suppressed because it is too large
Load Diff
+595
-356
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,7 @@
|
||||
"common": {
|
||||
"increase": "增高",
|
||||
"rating": "评分",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"refresh": "刷新",
|
||||
"unknown": "未知",
|
||||
"edit": "编辑",
|
||||
@@ -208,8 +208,8 @@
|
||||
"queue_clear": "清空播放队列",
|
||||
"muted": "已静音",
|
||||
"unfavorite": "取消收藏",
|
||||
"queue_moveToTop": "将所选项移至底部",
|
||||
"queue_moveToBottom": "将所选项移至顶部",
|
||||
"queue_moveToTop": "将所选项移至顶部",
|
||||
"queue_moveToBottom": "将所选项移至底部",
|
||||
"shuffle_off": "禁用随机播放",
|
||||
"addLast": "最后",
|
||||
"mute": "静音",
|
||||
@@ -240,12 +240,12 @@
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
|
||||
"hotkey_favoriteCurrentSong": "收藏$t(common.currentSong)",
|
||||
"audioExclusiveMode_description": "启用独占输出模式。在此模式下,系统通常被锁定为只有 mpv 能够输出音频",
|
||||
"audioExclusiveMode_description": "启用独占输出模式。在此模式下,系统通常被锁定为只有 MPV 能够输出音频",
|
||||
"disableLibraryUpdateOnStartup": "禁用启动时查询新版本",
|
||||
"gaplessAudio": "无缝音频",
|
||||
"audioPlayer_description": "选择用于播放的音频播放器",
|
||||
"globalMediaHotkeys": "全局媒体快捷键",
|
||||
"gaplessAudio_description": "调整 mpv 无缝音频设置",
|
||||
"gaplessAudio_description": "调整 MPV 无缝音频设置",
|
||||
"followLyric_description": "滚动歌词到当前播放位置",
|
||||
"audioExclusiveMode": "音频独占模式",
|
||||
"font": "字体",
|
||||
@@ -261,7 +261,7 @@
|
||||
"followLyric": "跟随当前歌词",
|
||||
"crossfadeDuration": "淡入淡出持续时间",
|
||||
"audioPlayer": "音频播放器",
|
||||
"discordApplicationId": "{{discord}} 应用 id",
|
||||
"discordApplicationId": "{{discord}} 应用 ID",
|
||||
"applicationHotkeys_description": "配置应用快捷键。勾选设为全局快捷键(仅桌面端)",
|
||||
"customFontPath_description": "设置应用使用的自定义字体路径",
|
||||
"gaplessAudio_optionWeak": "弱(推荐)",
|
||||
@@ -285,7 +285,7 @@
|
||||
"scrobble": "记录播放信息",
|
||||
"skipDuration_description": "设置每次按下跳过按钮将会跳过的时长",
|
||||
"fontType_optionSystem": "系统字体",
|
||||
"mpvExecutablePath_description": "设置 mpv 可执行文件的路径。如果留空,则使用默认路径",
|
||||
"mpvExecutablePath_description": "设置 MPV 可执行文件的路径。如果留空,则使用默认路径",
|
||||
"sampleRate": "采样率",
|
||||
"sidePlayQueueStyle_optionAttached": "吸附",
|
||||
"sidebarConfiguration": "侧边栏设定",
|
||||
@@ -334,7 +334,7 @@
|
||||
"hotkey_toggleShuffle": "切换随机",
|
||||
"theme": "主题",
|
||||
"playbackStyle_description": "选择音频播放器的播放风格",
|
||||
"mpvExecutablePath": "mpv 可执行文件路径",
|
||||
"mpvExecutablePath": "MPV 可执行文件路径",
|
||||
"hotkey_rate2": "评为 2 星",
|
||||
"playButtonBehavior_description": "设置将歌曲添加到播放队列时播放按钮的默认行为",
|
||||
"minimumScrobblePercentage_description": "歌曲被记录为已播放所需的最小播放百分比",
|
||||
@@ -344,7 +344,7 @@
|
||||
"savePlayQueue": "保存播放队列",
|
||||
"minimumScrobbleSeconds_description": "歌曲被记录为已播放所需的最小播放时间",
|
||||
"skipPlaylistPage_description": "打开歌单时,直接查看歌曲列表而非查看默认页面",
|
||||
"fontType_description": "内置字体可以选择 feishin 提供的字体之一。系统字体允许您选择操作系统提供的任何字体。自定义选项允许您使用自己的字体",
|
||||
"fontType_description": "内置字体可以选择 Feishin 提供的字体之一。系统字体允许您选择操作系统提供的任何字体。自定义选项允许您使用自己的字体",
|
||||
"playButtonBehavior": "播放按钮行为",
|
||||
"volumeWheelStep": "音量滚轮分度",
|
||||
"sidebarPlaylistList_description": "显示或隐藏侧边栏歌单列表",
|
||||
@@ -385,20 +385,20 @@
|
||||
"replayGainClipping_description": "自动降低增益以防止{{ReplayGain}}造成削波",
|
||||
"replayGainPreamp": "{{ReplayGain}}前置放大(分贝)",
|
||||
"replayGainClipping": "{{ReplayGain}}削波",
|
||||
"discordUpdateInterval": "{{discord}} rich presence 更新间隔",
|
||||
"discordApplicationId_description": "{{discord}} rich presence 应用 id(默认为 {{defaultId}})",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence 更新间隔",
|
||||
"discordApplicationId_description": "{{discord}} Rich Presence 应用 ID(默认为 {{defaultId}})",
|
||||
"discordUpdateInterval_description": "更新间隔秒数(至少 15 秒)",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordRichPresence_description": "在 {{discord}} Rich Presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"accentColor": "强调色",
|
||||
"accentColor_description": "设置应用的强调色",
|
||||
"replayGainPreamp_description": "调整应用在{{ReplayGain}}值上的前置放大增益",
|
||||
"discordIdleStatus": "显示 rich presence 闲置状态",
|
||||
"discordIdleStatus": "显示 Rich Presence 闲置状态",
|
||||
"clearCache": "清除浏览器缓存",
|
||||
"buttonSize": "播放器栏按钮大小",
|
||||
"buttonSize_description": "播放器栏按钮大小",
|
||||
"clearCache_description": "feishin的“硬清除”。除了清除feishin的缓存,清空浏览器缓存(保存的图像和其他资源)。服务器凭据和设置会被保留",
|
||||
"clearQueryCache_description": "feishin的“软清除”。这将会刷新播放列表、元数据并重置保存的歌词。设置、服务器凭据和缓存图像会被保留",
|
||||
"clearQueryCache": "清除feishin缓存",
|
||||
"clearCache_description": "Feishin的“硬清除”。除了清除Feishin的缓存,清空浏览器缓存(保存的图像和其他资源)。服务器凭据和设置会被保留",
|
||||
"clearQueryCache_description": "Feishin的“软清除”。这将会刷新播放列表、元数据并重置保存的歌词。设置、服务器凭据和缓存图像会被保留",
|
||||
"clearQueryCache": "清除Feishin缓存",
|
||||
"externalLinks": "显示外部链接",
|
||||
"externalLinks_description": "允许在艺术家/专辑页面上显示外部链接(Last.fm、MusicBrainz)",
|
||||
"mpvExtraParameters_help": "每行一个",
|
||||
@@ -420,10 +420,10 @@
|
||||
"contextMenu_description": "允许您隐藏右键单击项目时显示在菜单中的项目。未选中的项目将被隐藏",
|
||||
"customCssEnable_description": "允许编写自定义 css",
|
||||
"customCss": "自定义css",
|
||||
"customCss_description": "自定义css内容。注意:内容和远程url是不允许的属性。内容预览展示如下。出于安全考虑,您未设置的其它字段也会显示",
|
||||
"customCss_description": "自定义css内容。注意:内容和远程URL是不允许的属性。内容预览展示如下。出于安全考虑,您未设置的其它字段也会显示",
|
||||
"contextMenu": "上下文菜单(右键单击)配置",
|
||||
"customCssEnable": "启用自定义 css",
|
||||
"customCssNotice": "警告:虽然预设了一些安全限制(不允许 url() 和 content:),但使用自定义 css 仍然会因更改界面而带来风险",
|
||||
"customCssNotice": "警告:虽然预设了一些安全限制(不允许 URL() 和 content:),但使用自定义 css 仍然会因更改界面而带来风险",
|
||||
"transcode_description": "可以转码为不同的格式",
|
||||
"transcodeBitrate": "转码比特率",
|
||||
"albumBackground": "专辑背景图片",
|
||||
@@ -451,14 +451,14 @@
|
||||
"lastfmApiKey": "{{lastfm}} API 密钥",
|
||||
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需",
|
||||
"discordServeImage": "从服务器提供 {{discord}} 图像",
|
||||
"discordServeImage_description": "从服务器本身分享 {{discord}} rich presence 的封面艺术,仅适用于 Jellyfin 和 Navidrome。 {{discord}} 使用机器人来获取图像,因此您的服务器必须可通过公共互联网访问",
|
||||
"discordServeImage_description": "从服务器本身分享 {{discord}} Rich Presence 的封面艺术,仅适用于 Jellyfin 和 Navidrome。 {{discord}} 使用机器人来获取图像,因此您的服务器必须可通过公共互联网访问",
|
||||
"musicbrainz": "显示 MusicBrainz 链接",
|
||||
"musicbrainz_description": "在艺术家/专辑页面上显示 MusicBrainz 链接(如果存在 MusicBrainz ID)",
|
||||
"lastfm": "显示 last.fm 链接",
|
||||
"lastfm": "显示 Last.fm 链接",
|
||||
"lastfm_description": "在艺术家/专辑页面上显示 Last.fm 的链接",
|
||||
"preferLocalLyrics_description": "优先选择本地歌词(如有),而不是远程歌词",
|
||||
"preferLocalLyrics": "首选本地歌词",
|
||||
"discordPausedStatus": "暂停时显示rich presence",
|
||||
"discordPausedStatus": "暂停时显示Rich Presence",
|
||||
"discordPausedStatus_description": "启用后将在播放器暂停时显示状态",
|
||||
"preservePitch": "保持音高",
|
||||
"preservePitch_description": "在调整播放速度时保持音高",
|
||||
@@ -489,7 +489,7 @@
|
||||
"exportImportSettings_control_title": "导入/导出设置",
|
||||
"exportImportSettings_destructiveWarning": "导入设置会破坏现有设置,请在点击下方“导入”按钮前仔细阅读以上内容!",
|
||||
"exportImportSettings_importBtn": "导入设置",
|
||||
"exportImportSettings_importModalTitle": "导入 feishin 设置",
|
||||
"exportImportSettings_importModalTitle": "导入 Feishin 设置",
|
||||
"exportImportSettings_importSuccess": "设置已成功导入!",
|
||||
"exportImportSettings_notValidJSON": "传递的文件不是有效的 JSON 文件",
|
||||
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" 不正确 - {{reason}}",
|
||||
@@ -508,16 +508,15 @@
|
||||
"combinedLyricsAndVisualizer_description": "将歌词和可视化界面合并到同一面板中",
|
||||
"queryBuilderCustomFields_description": "在查询构建器添加自定义字段",
|
||||
"combinedLyricsAndVisualizer": "在播放器侧边栏合并歌词和可视化界面",
|
||||
"autoDJ_description": "自动添加相似歌曲到队列中",
|
||||
"notify_description": "歌曲变更时显示通知",
|
||||
"mpvExtraParameters_description": "向mpv传递额外参数",
|
||||
"mpvExtraParameters_description": "向MPV传递额外参数",
|
||||
"audioFadeOnStatusChange": "音频改变时淡入淡出",
|
||||
"showVisualizerInSidebar": "在播放器侧边栏显示可视化效果",
|
||||
"showLyricsInSidebar": "在播放器侧边栏显示歌词",
|
||||
"analyticsDisable": "退出使用情况的分析",
|
||||
"artistReleaseTypeConfiguration": "艺术家发行类型设置",
|
||||
"useThemeAccentColor": "使用主题强调色",
|
||||
"mpvExtraParameters": "mpv额外参数",
|
||||
"mpvExtraParameters": "MPV额外参数",
|
||||
"showRatings": "显示星级评分",
|
||||
"followCurrentSong": "跟随当前歌曲",
|
||||
"logLevel": "日志等级",
|
||||
@@ -552,7 +551,7 @@
|
||||
"autoDJ_timing": "定时",
|
||||
"autoDJ_timing_description": "自动 DJ 触发前队列中剩余的歌曲数量",
|
||||
"crossfadeStyle": "交叉渐变风格",
|
||||
"discordRichPresence": "{{discord}} rich presence",
|
||||
"discordRichPresence": "{{discord}} Rich Presence",
|
||||
"homeFeatureStyle_description": "控制首页特色轮播图的样式",
|
||||
"homeFeatureStyle": "首页特色旋转样式",
|
||||
"homeFeatureStyle_optionMultiple": "多样",
|
||||
@@ -586,7 +585,7 @@
|
||||
"automaticUpdates_description": "自动检查并安装更新",
|
||||
"releaseChannel_optionAlpha": "alpha(每日构建版)",
|
||||
"discordStateIcon": "显示播放图标",
|
||||
"discordStateIcon_description": "在 rich presence 状态中显示一个小的播放图标。启用“暂停时显示 rich presence 在线状态”后,暂停图标始终显示",
|
||||
"discordStateIcon_description": "在 Rich Presence 状态中显示一个小的播放图标。启用“暂停时显示 Rich Presence 在线状态”后,暂停图标始终显示",
|
||||
"blurExplicitImages": "模糊显式图片",
|
||||
"blurExplicitImages_description": "专辑和歌曲封面若被标记为不雅内容,将会进行模糊处理",
|
||||
"autosave": "自动保存播放队列",
|
||||
@@ -673,7 +672,7 @@
|
||||
"fromYear": "起始年份",
|
||||
"criticRating": "评论家评分",
|
||||
"trackNumber": "曲目",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"comment": "评论",
|
||||
"isCompilation": "为合辑",
|
||||
@@ -687,7 +686,7 @@
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"note": "注释",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2})数",
|
||||
"id": "id",
|
||||
"id": "ID",
|
||||
"disc": "碟片",
|
||||
"duration": "时长",
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
@@ -925,12 +924,12 @@
|
||||
"ignoreSsl": "忽略 ssl $t(common.restartRequired)",
|
||||
"ignoreCors": "忽略 cors $t(common.restartRequired)",
|
||||
"error_savePassword": "保存密码时出现错误",
|
||||
"input_url": "url",
|
||||
"input_url": "URL",
|
||||
"input_preferInstantMixDescription": "仅使用即时混音来获取类似的歌曲。如果您有修改此行为的插件,则很有用",
|
||||
"input_preferInstantMix": "首选即时混音",
|
||||
"input_preferRemoteUrl": "首选公共 url",
|
||||
"input_remoteUrl": "公共 url",
|
||||
"input_remoteUrlPlaceholder": "可选:对外功能的公共 url"
|
||||
"input_preferRemoteUrl": "首选公共 URL",
|
||||
"input_remoteUrl": "公共 URL",
|
||||
"input_remoteUrlPlaceholder": "可选:对外功能的公共 URL"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "添加$t(entity.trackWithCount, {\"count\": {{message}} })到$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
@@ -1015,6 +1014,9 @@
|
||||
"input_played_optionPlayed": "仅已播放的曲目",
|
||||
"input_limit": "有多少首歌?",
|
||||
"input_played": "播放筛选器"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "电台更新成功"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1063,7 +1065,7 @@
|
||||
"duration": "$t(common.duration)",
|
||||
"dateAdded": "添加日期",
|
||||
"size": "$t(common.size)",
|
||||
"bpm": "$t(common.bpm)",
|
||||
"bpm": "$t(common.BPM)",
|
||||
"lastPlayed": "最后播放",
|
||||
"trackNumber": "音轨编号",
|
||||
"rowIndex": "行索引",
|
||||
@@ -1109,7 +1111,7 @@
|
||||
"releaseDate": "发布日期",
|
||||
"bitrate": "比特率",
|
||||
"title": "标题",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"dateAdded": "添加日期",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||
|
||||
+243
-160
@@ -3,24 +3,24 @@
|
||||
"backward": "返回",
|
||||
"biography": "簡介",
|
||||
"bitrate": "位元率",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"clear": "清空",
|
||||
"collapse": "折疊",
|
||||
"collapse": "摺疊",
|
||||
"comingSoon": "即將推出…",
|
||||
"confirm": "確認",
|
||||
"decrease": "降低",
|
||||
"delete": "刪除",
|
||||
"descending": "降冪",
|
||||
"description": "描述",
|
||||
"forceRestartRequired": "重新啟動應用程式以使更改生效…關閉通知後即可重啟",
|
||||
"forceRestartRequired": "重啟以套用變更… 關閉通知後即可重啟",
|
||||
"menu": "選單",
|
||||
"action_other": "操作",
|
||||
"add": "新增",
|
||||
"areYouSure": "你確定嗎?",
|
||||
"ascending": "升冪",
|
||||
"disable": "禁用",
|
||||
"disable": "停用",
|
||||
"disc": "光碟",
|
||||
"dismiss": "不再顯示",
|
||||
"dismiss": "不理會",
|
||||
"duration": "時長",
|
||||
"edit": "編輯",
|
||||
"enable": "啟用",
|
||||
@@ -31,7 +31,7 @@
|
||||
"forward": "前進",
|
||||
"gap": "空隙",
|
||||
"home": "首頁",
|
||||
"increase": "增高",
|
||||
"increase": "提升",
|
||||
"left": "左",
|
||||
"limit": "限制",
|
||||
"manage": "管理",
|
||||
@@ -40,14 +40,14 @@
|
||||
"owner": "所有者",
|
||||
"path": "路徑",
|
||||
"playerMustBePaused": "播放器必須先暫停",
|
||||
"previousSong": "上一首$t(entity.track, {\"count\": 1})",
|
||||
"previousSong": "上一首 $t(entity.track, {\"count\": 1})",
|
||||
"quit": "退出",
|
||||
"random": "隨機",
|
||||
"rating": "評分",
|
||||
"refresh": "重新整理",
|
||||
"reset": "重置",
|
||||
"resetToDefault": "恢復為預設值",
|
||||
"restartRequired": "需要重新啟動應用程式",
|
||||
"resetToDefault": "重置為預設",
|
||||
"restartRequired": "需要重新啟動",
|
||||
"right": "右",
|
||||
"save": "儲存",
|
||||
"saveAndReplace": "儲存並取代",
|
||||
@@ -55,7 +55,7 @@
|
||||
"search": "搜尋",
|
||||
"sortOrder": "順序",
|
||||
"title": "標題",
|
||||
"trackNumber": "音軌編號",
|
||||
"trackNumber": "曲目",
|
||||
"unknown": "未知",
|
||||
"size": "大小",
|
||||
"version": "版本",
|
||||
@@ -64,24 +64,24 @@
|
||||
"cancel": "取消",
|
||||
"center": "中央",
|
||||
"channel_other": "聲道",
|
||||
"configure": "設定",
|
||||
"configure": "配置",
|
||||
"create": "建立",
|
||||
"currentSong": "目前$t(entity.track, {\"count\": 1})",
|
||||
"currentSong": "當前 $t(entity.track, {\"count\": 1})",
|
||||
"minimize": "最小化",
|
||||
"modified": "已修改",
|
||||
"name": "名稱",
|
||||
"no": "否",
|
||||
"none": "無",
|
||||
"noResultsFromQuery": "未查詢到匹配結果",
|
||||
"noResultsFromQuery": "查詢回傳了無結果",
|
||||
"note": "注釋",
|
||||
"additionalParticipants": "額外參與者",
|
||||
"newVersion": "已安裝新版本 ({{version}})",
|
||||
"newVersion": "新版本 ({{version}}) 已被安裝",
|
||||
"viewReleaseNotes": "查看發行註記",
|
||||
"albumGain": "專輯增益",
|
||||
"albumPeak": "專輯峰值",
|
||||
"bitDepth": "位元深度",
|
||||
"close": "關閉",
|
||||
"codec": "編碼",
|
||||
"codec": "編解碼器",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"preview": "預覽",
|
||||
"reload": "重新載入",
|
||||
@@ -90,7 +90,7 @@
|
||||
"share": "分享",
|
||||
"tags": "標籤",
|
||||
"trackGain": "曲目增益",
|
||||
"trackPeak": "歌曲峰值",
|
||||
"trackPeak": "曲目峰值",
|
||||
"translation": "翻譯",
|
||||
"doNotShowAgain": "不再顯示",
|
||||
"externalLinks": "外部連結",
|
||||
@@ -103,11 +103,11 @@
|
||||
"sort": "排序",
|
||||
"tableColumns": "表格欄位",
|
||||
"clean": "清除",
|
||||
"explicitStatus": "Explicit狀態",
|
||||
"explicit": "Explicit",
|
||||
"gridRows": "網格行",
|
||||
"noFilters": "未設定任何過濾器",
|
||||
"countSelected": "{{count}}個已選取",
|
||||
"explicitStatus": "露骨狀態",
|
||||
"explicit": "露骨",
|
||||
"gridRows": "網格列",
|
||||
"noFilters": "未配置篩選器",
|
||||
"countSelected": "{{count}} 個已選取",
|
||||
"retry": "重試",
|
||||
"example": "範例",
|
||||
"mood": "情緒",
|
||||
@@ -116,43 +116,45 @@
|
||||
"itemsMore": "{{count}} 更多",
|
||||
"filter_single": "單選",
|
||||
"filter_multiple": "複選",
|
||||
"newVersionAvailable": "有新的版本可供使用",
|
||||
"newVersionAvailable": "有新版本可用",
|
||||
"numberOfResults": "{{numberOfResults}} 項結果",
|
||||
"grouping": "分組"
|
||||
"grouping": "分組",
|
||||
"back": "返回",
|
||||
"openFolder": "開啟資料夾"
|
||||
},
|
||||
"error": {
|
||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||
"apiRouteError": "請求失敗:無法路由",
|
||||
"audioDeviceFetchError": "無法取得音訊設備",
|
||||
"endpointNotImplementedError": "{{serverType}} 尚未實作端點 {{endpoint}}",
|
||||
"apiRouteError": "無法路由請求",
|
||||
"audioDeviceFetchError": "嘗試取得音訊裝置時發生了錯誤",
|
||||
"authenticationFailed": "驗證失敗",
|
||||
"credentialsRequired": "需要憑證",
|
||||
"genericError": "發生了錯誤",
|
||||
"invalidServer": "無效的伺服器",
|
||||
"localFontAccessDenied": "無法取得本地字體",
|
||||
"localFontAccessDenied": "存取本地字型被拒絕",
|
||||
"loginRateError": "登入請求嘗試次數過多,請稍後再試",
|
||||
"remoteDisableError": "$t(common.disable)遠端伺服器時出現錯誤",
|
||||
"remoteEnableError": "$t(common.enable)遠端伺服器時出現錯誤",
|
||||
"remotePortError": "設定遠端伺服器連接埠時發生錯誤",
|
||||
"remotePortWarning": "重啟伺服器使新連接埠生效",
|
||||
"remoteDisableError": "嘗試 $t(common.disable) 遠端伺服器時發生了錯誤",
|
||||
"remoteEnableError": "嘗試 $t(common.enable) 遠端伺服器時發生了錯誤",
|
||||
"remotePortError": "嘗試設定遠端伺服器連接埠時發生了錯誤",
|
||||
"remotePortWarning": "重啟伺服器以套用新連接埠",
|
||||
"serverRequired": "需要伺服器",
|
||||
"sessionExpiredError": "工作階段已過期",
|
||||
"systemFontError": "嘗試取得系統字體時出現錯誤",
|
||||
"sessionExpiredError": "您的工作階段已過期",
|
||||
"systemFontError": "嘗試取得系統字型時發生了錯誤",
|
||||
"serverNotSelectedError": "未選擇伺服器",
|
||||
"mpvRequired": "需要 MPV",
|
||||
"playbackError": "無法播放媒體",
|
||||
"badAlbum": "您看到此頁面是因為這首歌不是專輯的一部分。如果您的音樂資料夾頂層有一首歌,則很可能會看到此問題。 Jellyfin 僅將資料夾中的曲目分組",
|
||||
"playbackError": "嘗試播放媒體時發生了錯誤",
|
||||
"badAlbum": "您看到此頁面是因為這首歌不是專輯的一部分。如果您的音樂資料夾頂層有一首歌,則很可能會看到此問題。 Jellyfin 僅會將資料夾中的曲目分組",
|
||||
"badValue": "無效選項「{{value}}」。該值不再存在",
|
||||
"networkError": "發生網路錯誤",
|
||||
"notificationDenied": "通知權限被拒絕。此設定無效",
|
||||
"networkError": "發生了網路錯誤",
|
||||
"notificationDenied": "通知權限被拒絕。此設定無影響",
|
||||
"openError": "無法開啟檔案",
|
||||
"multipleServerSaveQueueError": "播放佇列中包含不是來自目前伺服器的歌曲,此操作不受支援",
|
||||
"multipleServerSaveQueueError": "播放佇列中包含了並非來自當前伺服器的歌曲。此操作不受支援",
|
||||
"saveQueueFailed": "儲存播放佇列失敗",
|
||||
"settingsSyncError": "偵測到渲染器與主程式之間的設定不一致,請重新啟動應用程式以套用變更",
|
||||
"noNetwork": "伺服器無法連線",
|
||||
"noNetworkDescription": "無法連接到此伺服器",
|
||||
"settingsSyncError": "偵測到渲染器與主程式之間的設定不一致。請重新啟動應用程式以套用變更",
|
||||
"noNetwork": "伺服器不可用",
|
||||
"noNetworkDescription": "無法連線至此伺服器",
|
||||
"invalidJson": "無效的 JSON",
|
||||
"serverLockSingleServer": "當伺服器鎖定時只允許一個伺服器",
|
||||
"playbackPausedDueToError": "發生錯誤,已停止播放"
|
||||
"playbackPausedDueToError": "播放因錯誤而暫停"
|
||||
},
|
||||
"page": {
|
||||
"contextMenu": {
|
||||
@@ -197,12 +199,12 @@
|
||||
"title": "$t(common.home)",
|
||||
"mostPlayed": "最多播放",
|
||||
"newlyAdded": "最近新增的發行",
|
||||
"recentlyReleased": "最近發佈",
|
||||
"recentlyReleased": "最近發行",
|
||||
"genres": "$t(entity.genre, {\"count\": 2})"
|
||||
},
|
||||
"appMenu": {
|
||||
"openBrowserDevtools": "開啟瀏覽器開發者工具",
|
||||
"collapseSidebar": "折疊側邊欄",
|
||||
"collapseSidebar": "摺疊側邊欄",
|
||||
"expandSidebar": "展開側邊欄",
|
||||
"goBack": "返回",
|
||||
"goForward": "前進",
|
||||
@@ -221,7 +223,7 @@
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
"showLyricProvider": "顯示歌詞提供者",
|
||||
"useImageAspectRatio": "使用圖片縱橫比",
|
||||
"useImageAspectRatio": "使用圖片長寬比",
|
||||
"dynamicBackground": "動態背景",
|
||||
"followCurrentLyric": "跟隨目前歌詞",
|
||||
"lyricAlignment": "歌詞對齊",
|
||||
@@ -233,7 +235,9 @@
|
||||
"showLyricMatch": "顯示匹配的歌詞",
|
||||
"dynamicImageBlur": "圖片模糊大小",
|
||||
"dynamicIsImage": "啟用背景圖片",
|
||||
"lyricOffset": "歌詞偏移時間 (ms)"
|
||||
"lyricOffset": "歌詞偏移時間 (ms)",
|
||||
"lyricOpacityNonActive": "非活躍歌詞的不透明度",
|
||||
"lyricScaleNonActive": "非活躍歌詞的比例"
|
||||
},
|
||||
"lyrics": "歌詞",
|
||||
"related": "相關",
|
||||
@@ -265,7 +269,7 @@
|
||||
"transcoding": "轉碼",
|
||||
"discord": "Discord",
|
||||
"queryBuilder": "查詢建構器",
|
||||
"playerFilters": "播放過濾器",
|
||||
"playerFilters": "播放篩選器",
|
||||
"logger": "日誌記錄器",
|
||||
"lyricsDisplay": "歌詞顯示"
|
||||
},
|
||||
@@ -320,8 +324,8 @@
|
||||
"topSongsFrom": "{{title}} 的熱門歌曲",
|
||||
"viewAll": "檢視所有",
|
||||
"viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})",
|
||||
"groupingTypeAll": "所有發佈類型",
|
||||
"groupingTypePrimary": "主要發佈類型",
|
||||
"groupingTypeAll": "所有發行類型",
|
||||
"groupingTypePrimary": "主要發行類型",
|
||||
"favoriteSongs": "最愛歌曲",
|
||||
"favoriteSongsFrom": "{{title}} 的最愛歌曲",
|
||||
"topSongsCommunity": "社群",
|
||||
@@ -377,7 +381,7 @@
|
||||
"playbackSpeed": "播放速度",
|
||||
"playRandom": "隨機播放",
|
||||
"previous": "上一首",
|
||||
"queue_clear": "清空播放佇列",
|
||||
"queue_clear": "清空佇列",
|
||||
"queue_remove": "移除所選",
|
||||
"repeat": "循環",
|
||||
"repeat_all": "全部循環",
|
||||
@@ -394,8 +398,8 @@
|
||||
"next": "下一首",
|
||||
"play": "播放",
|
||||
"playbackFetchCancel": "請稍等…關閉通知以取消",
|
||||
"queue_moveToBottom": "使所選置頂",
|
||||
"queue_moveToTop": "使所選置底",
|
||||
"queue_moveToBottom": "使所選置底",
|
||||
"queue_moveToTop": "使所選置頂",
|
||||
"playSimilarSongs": "播放相似歌曲",
|
||||
"viewQueue": "檢視佇列",
|
||||
"addLastShuffled": "新增至尾端 (隨機)",
|
||||
@@ -415,7 +419,9 @@
|
||||
"sleepTimer_timeRemaining": "剩餘 {{time}}",
|
||||
"sleepTimer_setCustom": "設定定時器",
|
||||
"sleepTimer_cancel": "取消定時器",
|
||||
"albumRadio": "專輯電台"
|
||||
"albumRadio": "專輯電台",
|
||||
"scrobbleForceSubmit": "強制紀錄",
|
||||
"sleepTimer_endOfAlbum": "專輯播完時"
|
||||
},
|
||||
"setting": {
|
||||
"audioPlayer_description": "選擇用於播放的音訊播放器",
|
||||
@@ -424,7 +430,7 @@
|
||||
"hotkey_volumeDown": "音量降低",
|
||||
"hotkey_volumeMute": "靜音",
|
||||
"minimumScrobblePercentage": "最小紀錄時長(百分比)",
|
||||
"minimumScrobblePercentage_description": "歌曲被記錄為已播放(scrobble)所需的最小播放百分比",
|
||||
"minimumScrobblePercentage_description": "歌曲被記錄為已播放(Scrobble)所需的最小播放百分比",
|
||||
"theme_description": "設定應用程式的主題",
|
||||
"accentColor": "強調色",
|
||||
"accentColor_description": "設定應用程式的強調色",
|
||||
@@ -433,33 +439,33 @@
|
||||
"audioDevice": "音訊設備",
|
||||
"audioDevice_description": "選擇用於播放的音訊設備",
|
||||
"audioExclusiveMode": "音訊獨佔模式",
|
||||
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 mpv 能夠輸出音訊",
|
||||
"audioExclusiveMode_description": "啟用獨佔輸出模式。在此模式下,系統通常被鎖定,只有 MPV 能夠輸出音訊。視覺化音訊截取在此選項啟用時不會作用",
|
||||
"audioPlayer": "音訊播放器",
|
||||
"crossfadeDuration": "淡入淡出持續時間",
|
||||
"crossfadeDuration_description": "設定淡入淡出持續時間",
|
||||
"crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格",
|
||||
"customFontPath": "自定字體路徑",
|
||||
"customFontPath_description": "設定應用程式使用的自定字體路徑",
|
||||
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
|
||||
"discordApplicationId": "{{discord}} 應用程式 id",
|
||||
"discordApplicationId_description": "{{discord}} rich presence 應用程式 id(預設為 {{defaultId}})",
|
||||
"discordIdleStatus": "顯示 rich presence 閒置狀態",
|
||||
"customFontPath": "自訂字型路徑",
|
||||
"customFontPath_description": "設定應用程式要使用的自訂字型路徑",
|
||||
"disableLibraryUpdateOnStartup": "停用啟動時檢查新版本",
|
||||
"discordApplicationId": "{{discord}} 應用程式 ID",
|
||||
"discordApplicationId_description": "{{discord}} Rich Presence 應用程式 ID(預設為 {{defaultId}})",
|
||||
"discordIdleStatus": "顯示 Rich Presence 閒置狀態",
|
||||
"discordIdleStatus_description": "啟用後將會在播放器閒置時更新狀態",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵為:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} rich presence 更新間隔",
|
||||
"discordRichPresence_description": "在 {{discord}} Rich Presence 中顯示播放狀態。圖片鍵為:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} Rich Presence 更新間隔",
|
||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||
"enableRemote": "啟用遠端控制伺服器",
|
||||
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
|
||||
"exitToTray": "關閉時到將視窗最小化",
|
||||
"followLyric": "跟隨目前歌詞",
|
||||
"font_description": "設定應用程式使用的字體",
|
||||
"fontType": "字體類型",
|
||||
"fontType_description": "內建字體可以選擇 feishin 提供的字體之一。系統字體允許您選擇作業系統提供的任何字體。自定選項允許您使用自己的字體",
|
||||
"fontType_optionBuiltIn": "內建字體",
|
||||
"fontType_optionCustom": "自定字體",
|
||||
"fontType_optionSystem": "系統字體",
|
||||
"font_description": "設定應用程式使用的字型",
|
||||
"fontType": "字型類型",
|
||||
"fontType_description": "內建字型可以選擇 Feishin 提供的字型之一。系統字型允許您選擇作業系統提供的任何字型。自訂選項允許您使用自己的字型",
|
||||
"fontType_optionBuiltIn": "內建字型",
|
||||
"fontType_optionCustom": "自訂字型",
|
||||
"fontType_optionSystem": "系統字型",
|
||||
"gaplessAudio": "無間隔音訊",
|
||||
"gaplessAudio_description": "調整 mpv 無間隔音訊設定",
|
||||
"gaplessAudio_description": "調整 MPV 無間隔音訊設定",
|
||||
"gaplessAudio_optionWeak": "弱(建議)",
|
||||
"globalMediaHotkeys": "全域媒體快捷鍵",
|
||||
"hotkey_browserForward": "瀏覽器往前",
|
||||
@@ -497,9 +503,9 @@
|
||||
"lyricFetchProvider_description": "選擇歌詞來源",
|
||||
"minimizeToTray": "最小化到系統匣",
|
||||
"minimizeToTray_description": "將應用程式最小化到系統匣",
|
||||
"minimumScrobbleSeconds": "最小紀錄時間(秒)",
|
||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(scrobble)所需的最小播放時間",
|
||||
"mpvExecutablePath": "mpv 執行檔路徑",
|
||||
"minimumScrobbleSeconds": "最小紀錄時長(秒)",
|
||||
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(Scrobble)所需的最小播放時間",
|
||||
"mpvExecutablePath": "MPV 執行檔路徑",
|
||||
"playbackStyle_optionCrossFade": "淡入淡出",
|
||||
"playbackStyle_optionNormal": "一般",
|
||||
"playButtonBehavior": "播放按鈕動作",
|
||||
@@ -526,8 +532,8 @@
|
||||
"showSkipButton": "顯示跳過按鈕",
|
||||
"showSkipButton_description": "在播放條上顯示/隱藏跳過按鈕",
|
||||
"sidebarPlaylistList": "側邊欄播放清單列表",
|
||||
"sidebarCollapsedNavigation": "側邊欄(已折疊)導航",
|
||||
"sidebarCollapsedNavigation_description": "在折疊的側邊欄中顯示或隱藏導航",
|
||||
"sidebarCollapsedNavigation": "側邊欄(已摺疊)導航",
|
||||
"sidebarCollapsedNavigation_description": "在摺疊的側邊欄中顯示或隱藏導航",
|
||||
"sidebarConfiguration": "側邊欄設定",
|
||||
"sidebarConfiguration_description": "選擇側邊欄包含的項目與順序",
|
||||
"sidebarPlaylistList_description": "顯示或隱藏側邊欄歌單清單",
|
||||
@@ -555,19 +561,19 @@
|
||||
"playbackStyle": "播放風格",
|
||||
"exitToTray_description": "退出應用程式時最小化到系統匣而非關閉",
|
||||
"followLyric_description": "滾動歌詞到目前播放位置",
|
||||
"font": "字體",
|
||||
"globalMediaHotkeys_description": "啟用或禁用系統媒體快捷鍵以控制播放",
|
||||
"font": "字型",
|
||||
"globalMediaHotkeys_description": "啟用或停用系統媒體快捷鍵以控制播放",
|
||||
"hotkey_browserBack": "瀏覽器返回",
|
||||
"hotkey_favoriteCurrentSong": "收藏 $t(common.currentSong)",
|
||||
"hotkey_playbackStop": "停止",
|
||||
"hotkey_rate0": "清除評分",
|
||||
"mpvExecutablePath_description": "設定 mpv 執行檔的路徑。如果留空,則使用預設路徑",
|
||||
"mpvExecutablePath_description": "設定 MPV 執行檔的路徑。如果留空,則使用預設路徑",
|
||||
"playbackStyle_description": "選擇播放器的播放風格",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"remotePassword": "遠端控制伺服器密碼",
|
||||
"remotePassword_description": "設定遠端控制伺服器的密碼。這些憑證預設以不安全的方式傳輸,因此您應該使用一個您不在意的唯一密碼",
|
||||
"remotePort_description": "設定遠端控制伺服器的連接埠",
|
||||
"remoteUsername_description": "設定遠端控制伺服器的使用者名稱。如果使用者名稱和密碼都為空,則身分驗證將被禁用",
|
||||
"remoteUsername_description": "設定遠端控制伺服器的使用者名稱。如果使用者名稱和密碼都為空,則身分驗證將被停用",
|
||||
"replayGainClipping_description": "自動降低增益以防止{{ReplayGain}}造成削波",
|
||||
"showSkipButtons": "顯示跳過按鈕",
|
||||
"themeDark_description": "應用程式將使用深色主題",
|
||||
@@ -588,10 +594,10 @@
|
||||
"contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏",
|
||||
"customCssEnable": "啟用自訂CSS",
|
||||
"customCssEnable_description": "允許撰寫自訂CSS",
|
||||
"customCssNotice": "警告:即使已限制某些用法(不允許 url() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
||||
"customCssNotice": "警告:即使已限制某些用法(不允許 URL() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
||||
"customCss": "自訂CSS",
|
||||
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位",
|
||||
"discordPausedStatus": "暫停時顯示 rich presence",
|
||||
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位。桌面端:feishin在應用程式配置目錄中讀取和寫入custom.css,並在檔案更改時重新載入",
|
||||
"discordPausedStatus": "暫停時顯示 Rich Presence",
|
||||
"discordPausedStatus_description": "啟用後,播放器暫停時將顯示狀態",
|
||||
"discordListening": "將狀態設為\"正在聽\"",
|
||||
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
|
||||
@@ -607,13 +613,13 @@
|
||||
"homeFeature_description": "控制是否在首頁上顯示大型特色輪播",
|
||||
"imageAspectRatio": "使用原生封面照長寬比",
|
||||
"imageAspectRatio_description": "如果啟用,封面照將使用其原始長寬比顯示。對於非 1:1 的封面,剩餘空間將為空",
|
||||
"lastfm": "顯示 last.fm 連結",
|
||||
"lastfm": "顯示 Last.fm 連結",
|
||||
"lastfm_description": "在藝人/專輯頁面顯示 Last.fm 連結",
|
||||
"lastfmApiKey": "{{lastfm}} API金鑰",
|
||||
"lastfmApiKey_description": "{{lastfm}}的API金鑰。用於封面照",
|
||||
"mpvExtraParameters_help": "一行一個",
|
||||
"musicbrainz": "顯示 MusicBrainz 連結",
|
||||
"musicbrainz_description": "在存在 MusicBrainz ID 的藝人/專輯頁面上顯示 MusicBrainz 的連結",
|
||||
"musicbrainz_description": "在擁有 MusicBrainz ID 的藝人/專輯頁面上顯示 MusicBrainz 的連結",
|
||||
"neteaseTranslation": "啟用網易翻譯",
|
||||
"neteaseTranslation_description": "啟用後,將從網易取得並顯示翻譯的歌詞(如果有)",
|
||||
"passwordStore": "密碼/secret儲存",
|
||||
@@ -624,7 +630,7 @@
|
||||
"startMinimized": "啟動時最小化",
|
||||
"startMinimized_description": "在系統匣中啟動應用程式",
|
||||
"transcode_description": "啟用轉碼到不同格式",
|
||||
"transcodeBitrate": "要轉碼的位元率",
|
||||
"transcodeBitrate": "轉碼的位元率",
|
||||
"transcodeBitrate_description": "選擇要轉碼的位元率。 0 表示讓伺服器選擇",
|
||||
"transcodeFormat": "轉碼的格式",
|
||||
"transcodeFormat_description": "選擇要轉碼的格式。留空來讓伺服器決定",
|
||||
@@ -661,7 +667,7 @@
|
||||
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
|
||||
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
|
||||
"mediaSession": "啟用 Media Session",
|
||||
"mediaSession_description": "啟用 Media Session 整合功能,於系統音量 Overlay 和鎖定畫面中顯示媒體資料與控制面板",
|
||||
"mediaSession_description": "啟用 Media Session 整合功能,在系統音量疊加層和鎖定畫面上顯示媒體控制項與中繼資料。此功能需要使用網頁播放器。",
|
||||
"releaseChannel": "發佈通道",
|
||||
"analyticsDisable": "選擇退出使用情況分析",
|
||||
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
|
||||
@@ -706,16 +712,15 @@
|
||||
"queryBuilderCustomFields_description": "在查詢建構器中新增自訂欄位",
|
||||
"followCurrentSong_description": "自動將播放佇列捲動至當前播放的歌曲",
|
||||
"followCurrentSong": "跟隨當前歌曲",
|
||||
"playerbarSlider_description": "不建議在網路速度緩慢或計費的網路下使用波形",
|
||||
"playerFilters": "從佇列中過濾歌曲",
|
||||
"playerbarSlider_description": "不建議在速度緩慢或計費的網路下使用波形",
|
||||
"playerFilters": "從佇列中篩選歌曲",
|
||||
"playerFilters_description": "根據以下條件,排除要新增至佇列中的歌曲",
|
||||
"autoDJ": "Auto DJ",
|
||||
"autoDJ_description": "自動將相似的歌曲加入到播放佇列",
|
||||
"autoDJ_itemCount": "歌曲數量",
|
||||
"autoDJ_itemCount_description": "在啟用Auto DJ時嘗試加入佇列的歌曲數量",
|
||||
"autoDJ_itemCount": "項目數量",
|
||||
"autoDJ_itemCount_description": "嘗試加入佇列的項目數量",
|
||||
"autoDJ_timing_description": "佇列中剩餘多少歌曲時啟動 Auto DJ",
|
||||
"autoDJ_timing": "觸發時機",
|
||||
"logLevel": "log等級",
|
||||
"logLevel": "Log等級",
|
||||
"logLevel_description": "設定要顯示的最低日誌等級。Debug 會顯示所有日誌,Error 僅會顯示錯誤訊息",
|
||||
"logLevel_optionDebug": "Debug",
|
||||
"logLevel_optionError": "Error",
|
||||
@@ -723,7 +728,7 @@
|
||||
"logLevel_optionWarn": "Warn",
|
||||
"useThemeAccentColor": "使用主題強調色",
|
||||
"useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色",
|
||||
"artistRadioCount_description": "設定為藝人電台與曲目電台擷取的歌曲數量",
|
||||
"artistRadioCount_description": "設定要為藝人電台與曲目電台擷取的歌曲數量",
|
||||
"imageResolution": "圖片解析度",
|
||||
"imageResolution_description": "應用程式中所使用圖片的解析度。設定為 0 時,將使用圖片的原始解析度",
|
||||
"imageResolution_optionTable": "表格",
|
||||
@@ -758,17 +763,17 @@
|
||||
"enableGridMultiSelect_description": "啟用時,允許在網格檢視中選擇多項。停用時,單擊網格項目圖片將導航到項目頁面",
|
||||
"sidebarPlaylistSorting_description": "允許在側邊欄中使用拖放手動對播放清單進行排序,而不是預設的伺服器排序",
|
||||
"sidebarPlaylistListFilterRegex_description": "在側邊欄中隱藏與此正規表達式匹配的播放清單",
|
||||
"sidebarPlaylistListFilterRegex_placeholder": "範例: ^Daily Mix.*",
|
||||
"sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式",
|
||||
"sidebarPlaylistListFilterRegex_placeholder": "範例: ^daily mix.*",
|
||||
"sidebarPlaylistListFilterRegex": "播放清單篩選器正規表達式",
|
||||
"blurExplicitImages": "模糊露骨圖片",
|
||||
"blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊",
|
||||
"releaseChannel_optionAlpha": "alpha (每日建構版)",
|
||||
"releaseChannel_optionAlpha": "Alpha (每日建構版)",
|
||||
"analyticsEnable": "傳送基於使用情況的分析報告",
|
||||
"analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式",
|
||||
"automaticUpdates": "自動更新",
|
||||
"automaticUpdates_description": "自動檢查並安裝更新",
|
||||
"discordStateIcon": "顯示播放中圖示",
|
||||
"discordStateIcon_description": "在 rich presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 rich presence」時,會始終顯示暫停的圖示",
|
||||
"discordStateIcon_description": "在 Rich Presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 Rich Presence」時,會始終顯示暫停的圖示",
|
||||
"useThemePrimaryShade": "套用主題主色調",
|
||||
"useThemePrimaryShade_description": "使用所選主題中定義的主色調作為主色變體",
|
||||
"primaryShade": "主要色調",
|
||||
@@ -781,18 +786,85 @@
|
||||
"autosaveCount_description": "在儲存佇列之前,有多少曲目更改。1(最小)表示每次歌曲更改",
|
||||
"spotify_description": "在藝人與專輯頁面顯示 Spotify 的連結",
|
||||
"spotify": "顯示 Spotify 的連結",
|
||||
"nativeSpotify_description": "在 Spotify 應用程式中開啟,而非在瀏覽器中開啟",
|
||||
"nativeSpotify_description": "在 Spotify 應用程式而非瀏覽器中開啟",
|
||||
"nativeSpotify": "使用 Spotify 應用程式",
|
||||
"sidePlayQueueLayout": "側邊播放佇列佈局",
|
||||
"sidePlayQueueLayout_description": "設定吸附側邊播放佇列的佈局",
|
||||
"sidePlayQueueLayout_optionHorizontal": "水平",
|
||||
"sidePlayQueueLayout_optionVertical": "垂直",
|
||||
"listenbrainz_description": "在藝術家/專輯頁面上顯示 ListenBrainz 的連結",
|
||||
"listenbrainz_description": "在藝人/專輯頁面上顯示 ListenBrainz 的連結",
|
||||
"listenbrainz": "顯示 ListenBrainz 連結",
|
||||
"qobuz_description": "在藝術家/專輯頁面上顯示 Qobuz 的連結",
|
||||
"qobuz_description": "在藝人/專輯頁面上顯示 Qobuz 的連結",
|
||||
"qobuz": "顯示 Qobuz 連結",
|
||||
"waveformLoadingDelay": "波形載入延遲",
|
||||
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。"
|
||||
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。",
|
||||
"playerbarWaveformStretch": "波形拉伸",
|
||||
"playerbarWaveformStretch_description": "拉伸波形來填補可用空間",
|
||||
"preventSuspendOnPlayback_description": "音樂播放時防止應用程式進入休眠",
|
||||
"preventSuspendOnPlayback": "在播放時防止應用程式暫停",
|
||||
"hotkey_listShowPlayingSong": "在清單中顯示正在播放的歌曲",
|
||||
"sidebarPlaylistFolders_description": "為名稱中包含配置分隔符的播放清單建立資料夾檢視",
|
||||
"sidebarPlaylistFolders": "啟用資料夾",
|
||||
"sidebarPlaylistFolderSeparator_description": "在播放清單名稱中分隔資料夾級別的字元(或字串)",
|
||||
"sidebarPlaylistFolderSeparator": "資料夾分隔符",
|
||||
"sidebarPlaylistFolderView_description": "資料夾在側邊欄中顯示的方式",
|
||||
"sidebarPlaylistFolderView": "資料夾檢視",
|
||||
"sidebarPlaylistFolderView_optionSingle": "單一資料夾",
|
||||
"sidebarPlaylistFolderView_optionTree": "樹狀顯示",
|
||||
"sidebarPlaylistFolderView_optionNavigation": "導覽視圖",
|
||||
"sidebarPlaylistFolderTreeIndent_description": "每個樹狀層級縮排的像素",
|
||||
"sidebarPlaylistFolderTreeIndent": "樹狀縮排",
|
||||
"sidebarPlaylistFolderTreeLineColor_description": "樹狀連接線的顏色(留空則使用佈景主題預設值)",
|
||||
"sidebarPlaylistFolderTreeLineColor": "樹狀線條顏色",
|
||||
"sidebarPlaylistMode_description": "各播放清單在側邊欄列表中的顯示方式",
|
||||
"sidebarPlaylistMode": "側邊欄播放清單模式",
|
||||
"sidebarPlaylistMode_optionCompact": "緊湊",
|
||||
"sidebarPlaylistMode_optionExpanded": "展開",
|
||||
"autoDJ_mode": "模式",
|
||||
"autoDJ_mode_albums": "專輯",
|
||||
"autoDJ_mode_description": "選擇將歌曲或整張專輯加入佇列",
|
||||
"autoDJ_mode_songs": "歌曲",
|
||||
"autoDJ_enabled": "啟用Auto DJ",
|
||||
"autoDJ_albumStrategy": "專輯選擇模式",
|
||||
"autoDJ_songStrategy": "歌曲選擇模式",
|
||||
"autoDJ_strategy_option_library_random": "隨機",
|
||||
"autoDJ_strategy_option_similar": "相似",
|
||||
"enableFurigana_description": "在日文歌詞漢字上方顯示發音標記(振假名)。",
|
||||
"enableFurigana": "啟用振假名顯示",
|
||||
"equalizer_descriptionMpv": "透過 FFmpeg lavfi (MPV) 使用參數等化器",
|
||||
"equalizer_descriptionWebAudio": "透過 Web Audio API 使用參數等化器",
|
||||
"equalizer": "等化器",
|
||||
"equalizerBands_description": "各頻段增益。可上下拖曳或輸入數值。範圍:-12 至 +12 dB。",
|
||||
"equalizerBands": "頻段",
|
||||
"equalizerPreamp_description": "EQ 頻段之前的輸入增益。提升頻段時可設為負值以避免削波 (MPV)。",
|
||||
"equalizerPreamp": "前級增益",
|
||||
"equalizerPreset_description": "套用內建或已儲存的自訂 EQ 曲線",
|
||||
"equalizerPreset": "預設",
|
||||
"equalizerPresetDeletePlaceholder": "刪除自訂…",
|
||||
"equalizerPresetGroupBuiltIn": "內建",
|
||||
"equalizerPresetGroupCustom": "自訂",
|
||||
"equalizerPresetNamePlaceholder": "預設名稱…",
|
||||
"equalizerPresetSelectPlaceholder": "選擇預設",
|
||||
"equalizerSavePreset_description": "將目前 EQ 設定儲存為具名預設",
|
||||
"equalizerSavePreset": "儲存預設",
|
||||
"compressor_descriptionMpv": "透過 FFmpeg acompressor (MPV) 使用動態範圍壓縮器",
|
||||
"compressor_descriptionWebAudio": "透過 Web Audio API 使用動態範圍壓縮器",
|
||||
"compressor": "壓縮器",
|
||||
"compressorAttack_description": "訊號超過閾值後,壓縮器開始作用的速度。",
|
||||
"compressorAttack": "啟動時間",
|
||||
"compressorKnee_description": "柔性拐點寬度。數值越高,進入壓縮的過渡越平滑。",
|
||||
"compressorKnee": "拐點",
|
||||
"compressorMakeupGain_description": "壓縮後套用的輸出增益,用於恢復音量。",
|
||||
"compressorMakeupGain": "補償增益",
|
||||
"compressorPreset_description": "套用內建或已儲存的自訂壓縮器設定",
|
||||
"compressorRatio_description": "壓縮比例,例如 4 表示 4:1。",
|
||||
"compressorRatio": "比例",
|
||||
"compressorRelease_description": "訊號低於閾值後,壓縮器解除作用的速度。",
|
||||
"compressorRelease": "釋放時間",
|
||||
"compressorReset_description": "將所有壓縮器參數恢復為預設值",
|
||||
"compressorSavePreset_description": "將目前壓縮器設定儲存為具名預設",
|
||||
"compressorThreshold_description": "開始進行壓縮的訊號電平。",
|
||||
"compressorThreshold": "閥值"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -852,7 +924,7 @@
|
||||
"size": "$t(common.size)",
|
||||
"title": "$t(common.title)",
|
||||
"titleCombined": "$t(common.title)(合併)",
|
||||
"trackNumber": "曲目編號",
|
||||
"trackNumber": "曲目",
|
||||
"year": "$t(common.year)",
|
||||
"rating": "$t(common.rating)",
|
||||
"codec": "$t(common.codec)",
|
||||
@@ -876,11 +948,11 @@
|
||||
"column": {
|
||||
"album": "專輯",
|
||||
"albumArtist": "專輯藝人",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2})",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"albumCount": "專輯",
|
||||
"artist": "藝人",
|
||||
"biography": "簡介",
|
||||
"bitrate": "位元率",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"channels": "聲道",
|
||||
"comment": "評論",
|
||||
"dateAdded": "新增日期",
|
||||
"discNumber": "光碟",
|
||||
@@ -888,43 +960,43 @@
|
||||
"lastPlayed": "最後播放",
|
||||
"path": "路徑",
|
||||
"playCount": "播放次數",
|
||||
"rating": "評價",
|
||||
"releaseDate": "發布日期",
|
||||
"rating": "評分",
|
||||
"releaseDate": "發行日期",
|
||||
"releaseYear": "年份",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"bpm": "bpm",
|
||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||
"genre": "曲風",
|
||||
"bpm": "BPM",
|
||||
"songCount": "曲目",
|
||||
"title": "標題",
|
||||
"trackNumber": "曲目編號",
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)",
|
||||
"trackNumber": "曲目",
|
||||
"size": "大小",
|
||||
"codec": "編解碼器",
|
||||
"owner": "擁有者",
|
||||
"bitDepth": "$t(common.bitDepth)",
|
||||
"sampleRate": "$t(common.sampleRate)"
|
||||
"bitDepth": "位元深度",
|
||||
"sampleRate": "取樣率"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"addToFavorites": "新增到$t(entity.favorite, {\"count\": 2})",
|
||||
"clearQueue": "清空播放佇列",
|
||||
"createPlaylist": "建立$t(entity.playlist, {\"count\": 1})",
|
||||
"deletePlaylist": "刪除$t(entity.playlist, {\"count\": 1})",
|
||||
"addToPlaylist": "新增到$t(entity.playlist, {\"count\": 1})",
|
||||
"addToFavorites": "新增至 $t(entity.favorite, {\"count\": 2})",
|
||||
"clearQueue": "清空佇列",
|
||||
"createPlaylist": "建立 $t(entity.playlist, {\"count\": 1})",
|
||||
"deletePlaylist": "刪除 $t(entity.playlist, {\"count\": 1})",
|
||||
"addToPlaylist": "新增至 $t(entity.playlist, {\"count\": 1})",
|
||||
"deselectAll": "取消全選",
|
||||
"editPlaylist": "編輯 $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "前往頁面",
|
||||
"moveToBottom": "移至底部",
|
||||
"moveToTop": "移至頂部",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "從$t(entity.favorite, {\"count\": 2})移除",
|
||||
"removeFromPlaylist": "從$t(entity.playlist, {\"count\": 1})移除",
|
||||
"removeFromQueue": "從播放佇列中移除",
|
||||
"removeFromFavorites": "從 $t(entity.favorite, {\"count\": 2}) 移除",
|
||||
"removeFromPlaylist": "從 $t(entity.playlist, {\"count\": 1}) 移除",
|
||||
"removeFromQueue": "從佇列中移除",
|
||||
"setRating": "評分",
|
||||
"toggleSmartPlaylistEditor": "切換$t(entity.smartPlaylist)編輯器",
|
||||
"viewPlaylists": "查看$t(entity.playlist, {\"count\": 2})",
|
||||
"toggleSmartPlaylistEditor": "切換 $t(entity.smartPlaylist) 編輯器",
|
||||
"viewPlaylists": "查看 $t(entity.playlist, {\"count\": 2})",
|
||||
"moveToNext": "移至下一項",
|
||||
"openIn": {
|
||||
"lastfm": "在Last.fm開啟",
|
||||
"musicbrainz": "在MusicBrainz開啟",
|
||||
"lastfm": "在 Last.fm 中開啟",
|
||||
"musicbrainz": "在 MusicBrainz 開啟",
|
||||
"spotify": "在 Spotify 中開啟",
|
||||
"listenbrainz": "在 ListenBrainz 中開啟",
|
||||
"qobuz": "在 Qobuz 中開啟"
|
||||
@@ -945,7 +1017,9 @@
|
||||
"addOrRemoveFromSelection": "新增或移除選取項目",
|
||||
"selectAll": "全選",
|
||||
"selectRangeOfItems": "批量選取",
|
||||
"goToCurrent": "前往當前項目"
|
||||
"goToCurrent": "前往當前項目",
|
||||
"expandAllFolders": "展開所有資料夾",
|
||||
"collapseAllFolders": "摺疊所有資料夾"
|
||||
},
|
||||
"entity": {
|
||||
"album_other": "專輯",
|
||||
@@ -960,39 +1034,39 @@
|
||||
"genreWithCount_other": "{{count}} 種曲風",
|
||||
"playlist_other": "播放清單",
|
||||
"playlistWithCount_other": "{{count}} 個播放清單",
|
||||
"smartPlaylist": "智慧$t(entity.playlist, {\"count\": 1})",
|
||||
"smartPlaylist": "智慧 $t(entity.playlist, {\"count\": 1})",
|
||||
"track_other": "曲目",
|
||||
"trackWithCount_other": "{{count}} 首曲目",
|
||||
"trackWithCount_other": "{{count}} 個曲目",
|
||||
"albumWithCount_other": "{{count}} 張專輯",
|
||||
"play_other": "{{count}}次播放",
|
||||
"play_other": "{{count}} 次播放",
|
||||
"song_other": "歌曲",
|
||||
"radioStation_other": "電台",
|
||||
"radioStationWithCount_other": "{{count}} 個電台"
|
||||
},
|
||||
"filter": {
|
||||
"albumCount": "$t(entity.album, {\"count\": 2})數",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2}) 數",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "個人簡介",
|
||||
"biography": "簡介",
|
||||
"bitrate": "位元率",
|
||||
"bpm": "bpm",
|
||||
"bpm": "BPM",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"comment": "評論",
|
||||
"communityRating": "社群評分",
|
||||
"criticRating": "評論家評分",
|
||||
"dateAdded": "已新增日期",
|
||||
"dateAdded": "新增日期",
|
||||
"disc": "光碟",
|
||||
"duration": "時長",
|
||||
"id": "id",
|
||||
"id": "ID",
|
||||
"fromYear": "從年份",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"isCompilation": "為合輯",
|
||||
"isFavorited": "已收藏",
|
||||
"isPublic": "已公開",
|
||||
"isRated": "已評分",
|
||||
"isCompilation": "是否為合輯",
|
||||
"isFavorited": "是否為收藏",
|
||||
"isPublic": "是否為公開",
|
||||
"isRated": "是否已評分",
|
||||
"name": "名稱",
|
||||
"note": "注釋",
|
||||
"isRecentlyPlayed": "最近播放過",
|
||||
"lastPlayed": "上次播放過",
|
||||
"isRecentlyPlayed": "是否最近播放過",
|
||||
"lastPlayed": "上次播放",
|
||||
"mostPlayed": "播放最多",
|
||||
"owner": "$t(common.owner)",
|
||||
"path": "路徑",
|
||||
@@ -1001,16 +1075,16 @@
|
||||
"rating": "評分",
|
||||
"recentlyPlayed": "最近播放",
|
||||
"recentlyUpdated": "最近更新",
|
||||
"releaseDate": "發布日期",
|
||||
"releaseDate": "發行日期",
|
||||
"songCount": "曲目數",
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"favorited": "已收藏",
|
||||
"recentlyAdded": "最近新增",
|
||||
"releaseYear": "發布年份",
|
||||
"releaseYear": "發行年份",
|
||||
"search": "搜尋",
|
||||
"title": "標題",
|
||||
"toYear": "從年份",
|
||||
"toYear": "至年份",
|
||||
"trackNumber": "曲目",
|
||||
"explicitStatus": "$t(common.explicitStatus)",
|
||||
"sortName": "排序名稱",
|
||||
@@ -1023,13 +1097,13 @@
|
||||
"input_name": "伺服器名稱",
|
||||
"input_password": "密碼",
|
||||
"input_savePassword": "儲存密碼",
|
||||
"input_url": "url",
|
||||
"input_url": "URL",
|
||||
"input_username": "使用者名稱",
|
||||
"success": "伺服器新增成功",
|
||||
"title": "新增伺服器",
|
||||
"error_savePassword": "儲存密碼時出現錯誤",
|
||||
"ignoreCors": "忽略 cors $t(common.restartRequired)",
|
||||
"ignoreSsl": "忽略 ssl $t(common.restartRequired)",
|
||||
"ignoreCors": "忽略 CORS $t(common.restartRequired)",
|
||||
"ignoreSsl": "忽略 SSL $t(common.restartRequired)",
|
||||
"input_preferInstantMix": "偏好即時混音",
|
||||
"input_preferInstantMixDescription": "僅使用即時混音功能來取得相似歌曲。若您擁有能修改此行為的外掛,此功能將相當實用",
|
||||
"input_preferRemoteUrl": "優先使用公開網址",
|
||||
@@ -1042,7 +1116,8 @@
|
||||
"success": "新增 $t(entity.trackWithCount, {\"count\": {{message}} }) 到 $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "新增到$t(entity.playlist, {\"count\": 1})",
|
||||
"create": "建立 $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||
"searchOrCreate": "搜尋$t(entity.playlist, {\"count\": 2}) 或輸入內容以建立新項目"
|
||||
"searchOrCreate": "搜尋$t(entity.playlist, {\"count\": 2}) 或輸入內容以建立新項目",
|
||||
"noneAdded": "沒有曲目新增到 $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
@@ -1063,7 +1138,7 @@
|
||||
"title": "查詢編輯器",
|
||||
"addRuleGroup": "新增規則群組",
|
||||
"removeRuleGroup": "移除規則群組",
|
||||
"resetToDefault": "恢復為預設值",
|
||||
"resetToDefault": "重置為預設",
|
||||
"clearFilters": "清除篩選"
|
||||
},
|
||||
"updateServer": {
|
||||
@@ -1091,8 +1166,8 @@
|
||||
"successMustClick": "分享建立成功,點擊此處開啟"
|
||||
},
|
||||
"privateMode": {
|
||||
"enabled": "已啟用私人模式,播放狀態將對外部整合隱藏",
|
||||
"disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見",
|
||||
"enabled": "已啟用私人模式,播放狀態現在將對外部整合功能隱藏",
|
||||
"disabled": "已停用私人模式,啟用的外部整合功能現在可查看播放狀態",
|
||||
"title": "私人模式"
|
||||
},
|
||||
"largeFetchConfirmation": {
|
||||
@@ -1104,11 +1179,16 @@
|
||||
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"input_limit": "多少曲目?",
|
||||
"input_minYear": "起始年份",
|
||||
"input_maxYear": "結束年份",
|
||||
"input_played": "播放過濾器",
|
||||
"input_maxYear": "至年份",
|
||||
"input_played": "播放篩選器",
|
||||
"input_played_optionAll": "所有曲目",
|
||||
"input_played_optionUnplayed": "僅未播放的曲目",
|
||||
"input_played_optionPlayed": "僅播放過的曲目"
|
||||
"input_played_optionPlayed": "僅播放過的曲目",
|
||||
"input_kind_albums": "專輯",
|
||||
"input_kind_songs": "歌曲",
|
||||
"input_kind": "隨機選取",
|
||||
"input_limit_albums": "專輯數量?",
|
||||
"input_limit_songs": "歌曲數量?"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "電台建立成功",
|
||||
@@ -1126,7 +1206,7 @@
|
||||
"input_offset": "$t(setting.lyricOffset)"
|
||||
},
|
||||
"editRadioStation": {
|
||||
"success": "電臺更新成功"
|
||||
"success": "電台更新成功"
|
||||
}
|
||||
},
|
||||
"releaseType": {
|
||||
@@ -1145,7 +1225,7 @@
|
||||
"fieldRecording": "現場錄音",
|
||||
"demo": "Demo",
|
||||
"interview": "訪談",
|
||||
"live": "Live",
|
||||
"live": "現場演出",
|
||||
"mixtape": "混音帶",
|
||||
"remix": "Remix",
|
||||
"soundtrack": "原聲帶",
|
||||
@@ -1180,7 +1260,9 @@
|
||||
"notInPlaylist": "不在…之中",
|
||||
"startsWith": "以…開頭",
|
||||
"inTheLast": "在最後",
|
||||
"notInTheLast": "不在最後"
|
||||
"notInTheLast": "不在最後",
|
||||
"isMissing": "不存在",
|
||||
"isPresent": "存在"
|
||||
},
|
||||
"datetime": {
|
||||
"minuteShort": "分",
|
||||
@@ -1337,10 +1419,11 @@
|
||||
}
|
||||
},
|
||||
"systemAudioCaptureFailed": "無法開始擷取:{{message}}",
|
||||
"systemAudioNoAudioTrack": "沒有回傳任何音軌。確保在提示時啟用音訊擷取。",
|
||||
"systemAudioNoAudioTrack": "沒有回傳任何曲目。確保在提示時啟用音訊擷取。",
|
||||
"systemAudioConsentAllow": "允許",
|
||||
"systemAudioConsentBody": "此視覺化器需要存取系統音訊才能運作",
|
||||
"systemAudioConsentDecline": "拒絕",
|
||||
"systemAudioConsentTitle": "允許存取系統音訊?"
|
||||
"systemAudioConsentTitle": "允許存取系統音訊?",
|
||||
"systemAudioExclusiveModeNotSupported": "啟用音訊獨佔模式時,視覺化不可用。 在MPV設定中停用音訊獨佔模式,然後再試一次。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const disableAutoUpdates = () => {
|
||||
return process.env['DISABLE_AUTO_UPDATES'];
|
||||
};
|
||||
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
export const isWindows = () => {
|
||||
return process.platform === 'win32';
|
||||
};
|
||||
|
||||
export const isLinux = () => {
|
||||
return process.platform === 'linux';
|
||||
};
|
||||
@@ -4,3 +4,4 @@ import './player';
|
||||
import './remote';
|
||||
import './settings';
|
||||
import './discord-rpc';
|
||||
import './visualizer';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import Kuroshiro from 'kuroshiro';
|
||||
import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
|
||||
|
||||
// doc: https://kuroshiro.org
|
||||
|
||||
let kuroshiroInstance: any = null;
|
||||
let initPromise: null | Promise<void> = null;
|
||||
|
||||
const getKuroshiro = async () => {
|
||||
if (kuroshiroInstance) return kuroshiroInstance;
|
||||
if (initPromise) {
|
||||
await initPromise;
|
||||
return kuroshiroInstance;
|
||||
}
|
||||
|
||||
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||
kuroshiroInstance = new KuroshiroClass();
|
||||
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
|
||||
await initPromise;
|
||||
return kuroshiroInstance;
|
||||
};
|
||||
|
||||
export const convertFurigana = async (text: string): Promise<string> => {
|
||||
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||
|
||||
// check if the text contains any Japanese kana (to distinguish Japanese from Chinese text, which shares Kanji)
|
||||
// If no Japanese kana is detected, skip processing
|
||||
if (!KuroshiroClass.Util.hasKana(text)) return text;
|
||||
|
||||
try {
|
||||
const kuroshiro = await getKuroshiro();
|
||||
return await kuroshiro.convert(text, { mode: 'furigana', to: 'hiragana' });
|
||||
} catch (e) {
|
||||
console.error('Furigana conversion error: ', e);
|
||||
return text;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { store } from '../settings';
|
||||
import { convertFurigana } from './furigana';
|
||||
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
||||
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
||||
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
||||
@@ -231,3 +232,7 @@ ipcMain.handle('lyric-by-remote-id', async (_event, params: LyricGetQuery) => {
|
||||
const lyricResults = await getRemoteLyricsById(params);
|
||||
return lyricResults;
|
||||
});
|
||||
|
||||
ipcMain.handle('lyric-convert-furigana', async (_event, text: string) => {
|
||||
return await convertFurigana(text);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export const orderSearchResults = (args: {
|
||||
|
||||
searchResults = Array.from(combinedResults.values());
|
||||
} else {
|
||||
searchResults = fuse.search<InternetProviderLyricSearchResponse>({
|
||||
searchResults = fuse.search({
|
||||
...(params.artist && { artist: params.artist }),
|
||||
...(params.name && { name: params.name }),
|
||||
});
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import console from 'console';
|
||||
import { app, ipcMain } from 'electron';
|
||||
import { rm } from 'fs/promises';
|
||||
import { access, rm } from 'fs/promises';
|
||||
import uniq from 'lodash/uniq';
|
||||
import MpvAPI from 'node-mpv';
|
||||
import { pid } from 'node:process';
|
||||
import process from 'process';
|
||||
|
||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||
import { createLog, isWindows } from '../../../utils';
|
||||
import { createLog } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { isMacOS, isWindows } from '/@/main/env';
|
||||
import { PlayerData } from '/@/shared/types/domain-types';
|
||||
|
||||
declare module 'node-mpv';
|
||||
@@ -69,6 +70,7 @@ const mpvLog = (
|
||||
};
|
||||
|
||||
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
|
||||
const MACOS_MPV_BINARY_PATHS = ['/opt/homebrew/bin/mpv', '/usr/local/bin/mpv'];
|
||||
|
||||
const prefetchPlaylistParams = [
|
||||
'--prefetch-playlist=no',
|
||||
@@ -86,20 +88,52 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
|
||||
return parameters;
|
||||
};
|
||||
|
||||
const resolveMpvBinaryPath = async (binaryPath?: string) => {
|
||||
if (binaryPath) {
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
if (MPV_BINARY_PATH) {
|
||||
return MPV_BINARY_PATH;
|
||||
}
|
||||
|
||||
if (!isMacOS()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const candidate of MACOS_MPV_BINARY_PATHS) {
|
||||
try {
|
||||
await access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// Try the next common Homebrew location.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const createMpv = async (data: {
|
||||
binaryPath?: string;
|
||||
extraParameters?: string[];
|
||||
properties?: Record<string, any>;
|
||||
}): Promise<MpvAPI> => {
|
||||
const { binaryPath, extraParameters, properties } = data;
|
||||
const resolvedBinaryPath = await resolveMpvBinaryPath(binaryPath);
|
||||
const normalizedExtraParameters = (extraParameters ?? [])
|
||||
.map((param) => param.trim())
|
||||
.filter((param) => param.length > 0);
|
||||
|
||||
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
||||
const params = uniq([
|
||||
...DEFAULT_MPV_PARAMETERS(normalizedExtraParameters),
|
||||
...normalizedExtraParameters,
|
||||
]);
|
||||
|
||||
const mpv = new MpvAPI(
|
||||
{
|
||||
audio_only: true,
|
||||
auto_restart: false,
|
||||
binary: binaryPath || MPV_BINARY_PATH || undefined,
|
||||
binary: resolvedBinaryPath,
|
||||
socket: socketPath,
|
||||
time_update: 1,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
||||
|
||||
import { isLinux, isMacOS } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { isLinux, isMacOS } from '/@/main/env';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { deflate, gzip } from 'zlib';
|
||||
|
||||
import manifest from './manifest.json';
|
||||
|
||||
import { isLinux } from '/@/main/env';
|
||||
import { getMainWindow } from '/@/main/index';
|
||||
import { isLinux } from '/@/main/utils';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
||||
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import type { TitleTheme } from '/@/shared/types/types';
|
||||
import type { FSWatcher } from 'fs';
|
||||
|
||||
import { app, dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
nativeTheme,
|
||||
OpenDialogOptions,
|
||||
safeStorage,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { promises as fs, watch as fsWatch } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const getFrame = () => {
|
||||
@@ -26,6 +37,67 @@ const storePath = isDevelopment
|
||||
? path.normalize(`${defaultUserDataPath}-dev`)
|
||||
: path.normalize(defaultUserDataPath);
|
||||
|
||||
const CUSTOM_CSS_FILENAME = 'custom.css';
|
||||
const customCssPath = path.join(storePath, CUSTOM_CSS_FILENAME);
|
||||
let customCssWatcher: FSWatcher | null = null;
|
||||
let customCssDebounce: NodeJS.Timeout | null = null;
|
||||
|
||||
const readCustomCss = async (): Promise<{ content: string; exists: boolean }> => {
|
||||
try {
|
||||
const content = await fs.readFile(customCssPath, 'utf8');
|
||||
return { content, exists: true };
|
||||
} catch (error) {
|
||||
const fsError = error as NodeJS.ErrnoException;
|
||||
if (fsError.code === 'ENOENT') {
|
||||
return { content: '', exists: false };
|
||||
}
|
||||
|
||||
console.error('Failed to read custom css file', error);
|
||||
return { content: '', exists: false };
|
||||
}
|
||||
};
|
||||
|
||||
const notifyCustomCssUpdate = async () => {
|
||||
const { content, exists } = await readCustomCss();
|
||||
BrowserWindow.getAllWindows().forEach((window) => {
|
||||
window.webContents.send('custom-css-updated', {
|
||||
content,
|
||||
exists,
|
||||
path: customCssPath,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleCustomCssUpdate = () => {
|
||||
if (customCssDebounce) {
|
||||
clearTimeout(customCssDebounce);
|
||||
}
|
||||
|
||||
customCssDebounce = setTimeout(() => {
|
||||
notifyCustomCssUpdate().catch((error) => {
|
||||
console.error('Failed to broadcast custom css update', error);
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const startCustomCssWatcher = async () => {
|
||||
if (customCssWatcher) return;
|
||||
|
||||
try {
|
||||
await fs.mkdir(storePath, { recursive: true });
|
||||
customCssWatcher = fsWatch(storePath, (eventType, filename) => {
|
||||
if (!filename) return;
|
||||
if (filename.toString() !== CUSTOM_CSS_FILENAME) return;
|
||||
|
||||
if (eventType === 'change' || eventType === 'rename') {
|
||||
scheduleCustomCssUpdate();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to watch custom css file', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const store = new Store<any>({
|
||||
beforeEachMigration: (_store, context) => {
|
||||
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||
@@ -69,6 +141,14 @@ ipcMain.on('settings-set', (__event, data: { property: string; value: any }) =>
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('settings-set-sync', (__event, data: { property: string; value: any }) => {
|
||||
if (data.value === null) {
|
||||
store.delete(data.property);
|
||||
} else {
|
||||
store.set(data.property, data.value);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('password-get', (_event, server: string): null | string => {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
const servers = store.get('server') as Record<string, string> | undefined;
|
||||
@@ -120,3 +200,42 @@ ipcMain.handle('open-file-selector', async (_event, options: OpenDialogOptions)
|
||||
|
||||
return result.filePaths[0] || null;
|
||||
});
|
||||
|
||||
ipcMain.handle('custom-css-get', async () => {
|
||||
const { content, exists } = await readCustomCss();
|
||||
return {
|
||||
content,
|
||||
exists,
|
||||
path: customCssPath,
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('custom-css-save', async (_event, data: { content: string }) => {
|
||||
const content = typeof data?.content === 'string' ? data.content : '';
|
||||
await fs.mkdir(storePath, { recursive: true });
|
||||
await fs.writeFile(customCssPath, content, 'utf8');
|
||||
await notifyCustomCssUpdate();
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('custom-css-open-folder', async () => {
|
||||
await fs.mkdir(storePath, { recursive: true });
|
||||
await shell.openPath(storePath);
|
||||
return true;
|
||||
});
|
||||
|
||||
app.whenReady()
|
||||
.then(() => startCustomCssWatcher())
|
||||
.catch((error) => console.error('Failed to start custom css watcher', error));
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (customCssWatcher) {
|
||||
customCssWatcher.close();
|
||||
customCssWatcher = null;
|
||||
}
|
||||
|
||||
if (customCssDebounce) {
|
||||
clearTimeout(customCssDebounce);
|
||||
customCssDebounce = null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { getMpvInstance } from '../player';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
let isLocalVisualizerSurfaceVisible = false;
|
||||
|
||||
export const setLocalVisualizerSurfaceVisible = (visible: boolean) => {
|
||||
isLocalVisualizerSurfaceVisible = visible;
|
||||
};
|
||||
|
||||
export const canHandleVisualizerDisplayMedia = (): boolean => {
|
||||
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
|
||||
if (playbackType !== PlayerType.LOCAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLocalVisualizerSurfaceVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(getMpvInstance()?.isRunning());
|
||||
};
|
||||
|
||||
ipcMain.on('visualizer-set-local-surface-visible', (_event, visible: boolean) => {
|
||||
setLocalVisualizerSurfaceVisible(Boolean(visible));
|
||||
});
|
||||
@@ -1,2 +1,9 @@
|
||||
import './core';
|
||||
import(`./${process.platform}`);
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
import('./linux');
|
||||
} else if (process.platform === 'darwin') {
|
||||
import('./darwin');
|
||||
} else if (process.platform === 'win32') {
|
||||
import('./win32');
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
|
||||
+133
-28
@@ -5,6 +5,7 @@ import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
desktopCapturer,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
Menu,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
protocol,
|
||||
Rectangle,
|
||||
screen,
|
||||
session,
|
||||
shell,
|
||||
Tray,
|
||||
} from 'electron';
|
||||
@@ -29,18 +31,12 @@ import packageJson from '../../package.json';
|
||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||
import { shutdownServer } from './features/core/remote';
|
||||
import { store } from './features/core/settings';
|
||||
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||
import {
|
||||
autoUpdaterLogInterface,
|
||||
createLog,
|
||||
disableAutoUpdates,
|
||||
hotkeyToElectronAccelerator,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
isWindows,
|
||||
} from './utils';
|
||||
import './features';
|
||||
import { autoUpdaterLogInterface, createLog, hotkeyToElectronAccelerator } from './utils';
|
||||
|
||||
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '/@/main/env';
|
||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
const ALPHA_UPDATER_CONFIG: {
|
||||
@@ -256,7 +252,9 @@ function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdate
|
||||
return new NsisUpdater(ALPHA_UPDATER_CONFIG);
|
||||
}
|
||||
|
||||
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ privileges: { bypassCSP: true, corsEnabled: true }, scheme: 'feishin' },
|
||||
]);
|
||||
|
||||
process.on('uncaughtException', (error: any) => {
|
||||
console.error('Error in main process', error);
|
||||
@@ -284,6 +282,16 @@ let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
||||
let currentSidebarCollapsed = false;
|
||||
let currentShuffleEnabled = false;
|
||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
||||
let inputFocused = false;
|
||||
|
||||
ipcMain.on('input-focus-state', (_event, focused: boolean) => {
|
||||
const next = !!focused;
|
||||
if (inputFocused === next) return;
|
||||
inputFocused = next;
|
||||
if (isMacOS()) {
|
||||
rebuildMainMenu();
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
import('source-map-support').then((sourceMapSupport) => {
|
||||
@@ -329,7 +337,7 @@ if (isDevelopment) {
|
||||
}
|
||||
|
||||
const RESOURCES_PATH = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'assets')
|
||||
? path.join(path.dirname(app.getAppPath()), 'assets')
|
||||
: path.join(__dirname, '../../assets');
|
||||
|
||||
const getAssetPath = (...paths: string[]): string => {
|
||||
@@ -344,7 +352,7 @@ const rebuildMainMenu = () => {
|
||||
if (!menuBuilder || !mainWindow) return;
|
||||
|
||||
menuBuilder.buildMenu({
|
||||
accelerators: playbackMenuAccelerators,
|
||||
accelerators: inputFocused ? {} : playbackMenuAccelerators,
|
||||
playbackStatus: currentPlaybackStatus,
|
||||
privateMode: currentPrivateMode,
|
||||
repeatMode: currentRepeatMode,
|
||||
@@ -475,6 +483,15 @@ const createTray = () => {
|
||||
tray.setContextMenu(contextMenu);
|
||||
};
|
||||
|
||||
const validateUrl = (url: string): boolean => {
|
||||
// Minor security, really. Enforce only loading websites (http/https). file://
|
||||
// URLs and the like should've already been blocked, but this is another check.
|
||||
// Note that arbitrary web URLs are still allowed under this scheme, although
|
||||
// that should really only be hit by Subsonic share url (or if artist homepage
|
||||
// is allowed for ND extensions)
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
};
|
||||
|
||||
async function createWindow(first = true): Promise<void> {
|
||||
if (isDevelopment) {
|
||||
await installExtensions().catch(console.log);
|
||||
@@ -514,9 +531,9 @@ async function createWindow(first = true): Promise<void> {
|
||||
backgroundThrottling: false,
|
||||
contextIsolation: true,
|
||||
devTools: true,
|
||||
nodeIntegration: true,
|
||||
nodeIntegration: false,
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
sandbox: true,
|
||||
webSecurity: !store.get('ignore_cors'),
|
||||
},
|
||||
width: 1440,
|
||||
@@ -728,12 +745,38 @@ async function createWindow(first = true): Promise<void> {
|
||||
|
||||
// Open URLs in the user's browser
|
||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||
shell.openExternal(edata.url);
|
||||
if (validateUrl(edata.url)) {
|
||||
shell.openExternal(edata.url);
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||
callback({ audio: 'loopback' });
|
||||
if (!canHandleVisualizerDisplayMedia()) {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMacOS()) {
|
||||
callback({ audio: 'loopback' });
|
||||
return;
|
||||
}
|
||||
|
||||
desktopCapturer
|
||||
.getSources({ thumbnailSize: { height: 0, width: 0 }, types: ['screen'] })
|
||||
.then((sources) => {
|
||||
const source = sources[0];
|
||||
if (!source) {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
callback({ audio: 'loopback', video: source });
|
||||
})
|
||||
.catch((err) => {
|
||||
log.warn('desktopCapturer.getSources failed', err);
|
||||
callback({});
|
||||
});
|
||||
});
|
||||
|
||||
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||
@@ -744,7 +787,9 @@ async function createWindow(first = true): Promise<void> {
|
||||
nativeTheme.themeSource = theme || 'dark';
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
if (validateUrl(details.url)) {
|
||||
shell.openExternal(details.url);
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
@@ -911,12 +956,14 @@ ipcMain.on(
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle('power-save-blocker-start', () => {
|
||||
ipcMain.handle('power-save-blocker-start', (_event, { full }: { full: boolean }) => {
|
||||
if (powerSaveBlockerId !== null) {
|
||||
return powerSaveBlockerId;
|
||||
}
|
||||
|
||||
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
|
||||
powerSaveBlockerId = powerSaveBlocker.start(
|
||||
full ? 'prevent-display-sleep' : 'prevent-app-suspension',
|
||||
);
|
||||
return powerSaveBlockerId;
|
||||
});
|
||||
|
||||
@@ -944,14 +991,33 @@ app.on('window-all-closed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const FONT_HEADERS = [
|
||||
const FONT_HEADERS = new Set([
|
||||
'font/collection',
|
||||
'font/otf',
|
||||
'font/sfnt',
|
||||
'font/ttf',
|
||||
'font/woff',
|
||||
'font/woff2',
|
||||
];
|
||||
]);
|
||||
|
||||
const bytesToInt = (array: Uint8Array, length: number): number => {
|
||||
let value = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
value = (value << 8) + array[i];
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const FONT_FOUR_BYTE_MAGIC_NUMBERS = new Set([
|
||||
0x4f54544f, // font/otf
|
||||
0x774f4632, // font/woff2
|
||||
0x774f4646, // font/woff
|
||||
]);
|
||||
|
||||
const FONT_FIVE_BYTE_MAGIC_NUMBERS = new Set([
|
||||
0x0001000000, // ttf, collection, sfnt
|
||||
]);
|
||||
|
||||
const singleInstance = isDevelopment ? true : app.requestSingleInstanceLock();
|
||||
|
||||
@@ -972,12 +1038,9 @@ if (!singleInstance) {
|
||||
|
||||
app.whenReady()
|
||||
.then(() => {
|
||||
protocol.handle('feishin', async (request) => {
|
||||
const filePath = `file:${request.url.slice('feishin:'.length)}`;
|
||||
const response = await net.fetch(filePath);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (!contentType || !FONT_HEADERS.includes(contentType)) {
|
||||
protocol.handle('feishin', async () => {
|
||||
const filePath = store.get('local_font_path');
|
||||
if (typeof filePath !== 'string') {
|
||||
getMainWindow()?.webContents.send('custom-font-error', filePath);
|
||||
|
||||
return new Response(null, {
|
||||
@@ -986,7 +1049,49 @@ if (!singleInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
const response = await net.fetch('file:' + filePath);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// On Linux, the mime type is included in the response header
|
||||
// In this case, we can forward the response with no further processing
|
||||
if (contentType && FONT_HEADERS.has(contentType)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Otherwise, let's check the magic number to see if
|
||||
// the file is a font type. This is either four or five bytes
|
||||
const payload = await response.arrayBuffer();
|
||||
const magicNumber = new Uint8Array(payload.slice(0, 5));
|
||||
const fiveHex = bytesToInt(magicNumber, 5);
|
||||
const fourHex = bytesToInt(magicNumber, 4);
|
||||
|
||||
if (
|
||||
FONT_FIVE_BYTE_MAGIC_NUMBERS.has(fiveHex) ||
|
||||
FONT_FOUR_BYTE_MAGIC_NUMBERS.has(fourHex)
|
||||
) {
|
||||
// We have to create a new response with the payload, since it has been read now
|
||||
return new Response(payload, {
|
||||
headers: response.headers,
|
||||
});
|
||||
}
|
||||
|
||||
getMainWindow()?.webContents.send('custom-font-error', filePath);
|
||||
|
||||
return new Response(null, {
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
"script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://umami.jeffvli.org; style-src 'self' 'unsafe-inline'; media-src 'self' http: https: data: blob:; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; default-src 'self';",
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
createWindow();
|
||||
|
||||
@@ -18,22 +18,6 @@ if (process.env.NODE_ENV === 'development') {
|
||||
};
|
||||
}
|
||||
|
||||
export const disableAutoUpdates = () => {
|
||||
return process.env['DISABLE_AUTO_UPDATES'];
|
||||
};
|
||||
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
export const isWindows = () => {
|
||||
return process.platform === 'win32';
|
||||
};
|
||||
|
||||
export const isLinux = () => {
|
||||
return process.platform === 'linux';
|
||||
};
|
||||
|
||||
export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
||||
let accelerator = hotkey;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SetActivity } from '@xhayper/discord-rpc';
|
||||
import type { SetActivity } from '@xhayper/discord-rpc';
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
const initialize = (clientId: string) => {
|
||||
|
||||
Vendored
-3
@@ -1,11 +1,8 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||
|
||||
import { PreloadApi } from './index';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
api: PreloadApi;
|
||||
electron: ElectronAPI;
|
||||
LEGACY_AUTHENTICATION?: boolean;
|
||||
queryLocalFonts?: () => Promise<Font[]>;
|
||||
REMOTE_URL?: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload';
|
||||
import { contextBridge } from 'electron';
|
||||
import { contextBridge, webUtils } from 'electron';
|
||||
|
||||
import { autodiscover } from './autodiscover';
|
||||
import { browser } from './browser';
|
||||
@@ -11,12 +10,14 @@ import { mpris } from './mpris';
|
||||
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
||||
import { remote } from './remote';
|
||||
import { utils } from './utils';
|
||||
import { visualizer } from './visualizer';
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
autodiscover,
|
||||
browser,
|
||||
discordRpc,
|
||||
getPathForFile: webUtils.getPathForFile,
|
||||
ipc,
|
||||
localSettings,
|
||||
lyrics,
|
||||
@@ -25,6 +26,7 @@ const api = {
|
||||
mpvPlayerListener,
|
||||
remote,
|
||||
utils,
|
||||
visualizer,
|
||||
};
|
||||
|
||||
export type PreloadApi = typeof api;
|
||||
@@ -34,14 +36,11 @@ export type PreloadApi = typeof api;
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||
contextBridge.exposeInMainWorld('api', api);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI;
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api;
|
||||
}
|
||||
|
||||
@@ -8,21 +8,11 @@ const send = (channel: string, ...args: any[]) => {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
};
|
||||
|
||||
const invoke = (channel: string, ...args: any[]) => {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
};
|
||||
|
||||
const on = (channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||
ipcRenderer.on(channel, listener);
|
||||
};
|
||||
|
||||
const removeListener = (channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||
ipcRenderer.removeListener(channel, listener);
|
||||
};
|
||||
|
||||
export const ipc = {
|
||||
invoke,
|
||||
on,
|
||||
removeAllListeners,
|
||||
removeListener,
|
||||
send,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ipcRenderer, IpcRendererEvent, OpenDialogOptions, webFrame } from 'electron';
|
||||
import { ipcRenderer, OpenDialogOptions, webFrame } from 'electron';
|
||||
|
||||
import { TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
@@ -9,6 +9,13 @@ const set = (
|
||||
ipcRenderer.send('settings-set', { property, value });
|
||||
};
|
||||
|
||||
const setSync = async (
|
||||
property: string,
|
||||
value: boolean | null | Record<string, unknown> | string | string[],
|
||||
) => {
|
||||
return ipcRenderer.invoke('settings-set-sync', { property, value });
|
||||
};
|
||||
|
||||
const get = async (property: string) => {
|
||||
return ipcRenderer.invoke('settings-get', { property });
|
||||
};
|
||||
@@ -41,8 +48,8 @@ const setZoomFactor = (zoomFactor: number) => {
|
||||
webFrame.setZoomFactor(zoomFactor / 100);
|
||||
};
|
||||
|
||||
const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => {
|
||||
ipcRenderer.on('custom-font-error', cb);
|
||||
const fontError = (cb: (file: string) => void) => {
|
||||
ipcRenderer.on('custom-font-error', (_, file) => cb(file));
|
||||
};
|
||||
|
||||
const themeSet = (theme: TitleTheme): void => {
|
||||
@@ -99,6 +106,7 @@ export const localSettings = {
|
||||
passwordSet,
|
||||
restart,
|
||||
set,
|
||||
setSync,
|
||||
setZoomFactor,
|
||||
themeSet,
|
||||
};
|
||||
|
||||
@@ -26,7 +26,12 @@ const getRemoteLyricsByRemoteId = (id: LyricGetQuery) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const convertFurigana = (text: string): Promise<string> => {
|
||||
return ipcRenderer.invoke('lyric-convert-furigana', text);
|
||||
};
|
||||
|
||||
export const lyrics = {
|
||||
convertFurigana,
|
||||
getRemoteLyricsByRemoteId,
|
||||
getRemoteLyricsBySong,
|
||||
searchRemoteLyrics,
|
||||
|
||||
+11
-15
@@ -1,4 +1,4 @@
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
@@ -31,28 +31,24 @@ const updateSong = (song: QueueSong | undefined, imageUrl?: null | string) => {
|
||||
ipcRenderer.send('update-song', song, imageUrl);
|
||||
};
|
||||
|
||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||
ipcRenderer.on('request-seek', cb);
|
||||
const requestSeek = (cb: (data: { offset: number }) => void) => {
|
||||
ipcRenderer.on('request-seek', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
||||
ipcRenderer.on('request-position', cb);
|
||||
const requestPosition = (cb: (data: { position: number }) => void) => {
|
||||
ipcRenderer.on('request-position', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestToggleRepeat = (
|
||||
cb: (event: IpcRendererEvent, data: { repeat: PlayerRepeat }) => void,
|
||||
) => {
|
||||
ipcRenderer.on('mpris-request-toggle-repeat', cb);
|
||||
const requestToggleRepeat = (cb: (data: { repeat: PlayerRepeat }) => void) => {
|
||||
ipcRenderer.on('mpris-request-toggle-repeat', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestToggleShuffle = (
|
||||
cb: (event: IpcRendererEvent, data: { shuffle: boolean }) => void,
|
||||
) => {
|
||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
||||
const requestToggleShuffle = (cb: (data: { shuffle: boolean }) => void) => {
|
||||
ipcRenderer.on('mpris-request-toggle-shuffle', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('request-volume', cb);
|
||||
const requestVolume = (cb: (data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('request-volume', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
export const mpris = {
|
||||
|
||||
+37
-37
@@ -1,4 +1,4 @@
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { PlayerData } from '/@/shared/types/domain-types';
|
||||
|
||||
@@ -102,76 +102,76 @@ const getAudioDevices = async () => {
|
||||
return ipcRenderer.invoke('player-get-audio-devices');
|
||||
};
|
||||
|
||||
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-auto-next', cb);
|
||||
const rendererAutoNext = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-auto-next', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererCurrentTime = (cb: (event: IpcRendererEvent, data: number) => void) => {
|
||||
ipcRenderer.on('renderer-player-current-time', cb);
|
||||
const rendererCurrentTime = (cb: (data: number) => void) => {
|
||||
ipcRenderer.on('renderer-player-current-time', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-next', cb);
|
||||
const rendererNext = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-next', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-pause', cb);
|
||||
const rendererPause = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-pause', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererPlay = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-play', cb);
|
||||
const rendererPlay = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-play', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererPlayPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-play-pause', cb);
|
||||
const rendererPlayPause = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-play-pause', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererPrevious = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-previous', cb);
|
||||
const rendererPrevious = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-previous', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererStop = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-stop', cb);
|
||||
const rendererStop = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-stop', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererSkipForward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-skip-forward', cb);
|
||||
const rendererSkipForward = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-skip-forward', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererSkipBackward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-skip-backward', cb);
|
||||
const rendererSkipBackward = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-skip-backward', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererVolumeUp = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-volume-up', cb);
|
||||
const rendererVolumeUp = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-volume-up', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererVolumeDown = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-volume-down', cb);
|
||||
const rendererVolumeDown = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-volume-down', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererVolumeMute = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-volume-mute', cb);
|
||||
const rendererVolumeMute = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-volume-mute', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererToggleRepeat = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-toggle-repeat', cb);
|
||||
const rendererToggleRepeat = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-toggle-repeat', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererToggleShuffle = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-toggle-shuffle', cb);
|
||||
const rendererToggleShuffle = (cb: (data: PlayerData) => void) => {
|
||||
ipcRenderer.on('renderer-player-toggle-shuffle', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-player-quit', cb);
|
||||
const rendererQuit = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-player-quit', () => cb());
|
||||
};
|
||||
|
||||
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
||||
ipcRenderer.on('renderer-player-error', cb);
|
||||
const rendererError = (cb: (data: string) => void) => {
|
||||
ipcRenderer.on('renderer-player-error', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const rendererPlayerFallback = (cb: (event: IpcRendererEvent, data: boolean) => void) => {
|
||||
ipcRenderer.on('renderer-player-fallback', cb);
|
||||
const rendererPlayerFallback = (cb: (data: boolean) => void) => {
|
||||
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
export const mpvPlayer = {
|
||||
|
||||
+11
-16
@@ -1,33 +1,28 @@
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
const requestFavorite = (
|
||||
cb: (
|
||||
event: IpcRendererEvent,
|
||||
data: { favorite: boolean; id: string; serverId: string },
|
||||
) => void,
|
||||
cb: (data: { favorite: boolean; id: string; serverId: string }) => void,
|
||||
) => {
|
||||
ipcRenderer.on('request-favorite', cb);
|
||||
ipcRenderer.on('request-favorite', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
||||
ipcRenderer.on('request-position', cb);
|
||||
const requestPosition = (cb: (data: { position: number }) => void) => {
|
||||
ipcRenderer.on('request-position', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestRating = (
|
||||
cb: (event: IpcRendererEvent, data: { id: string; rating: number; serverId: string }) => void,
|
||||
) => {
|
||||
ipcRenderer.on('request-rating', cb);
|
||||
const requestRating = (cb: (data: { id: string; rating: number; serverId: string }) => void) => {
|
||||
ipcRenderer.on('request-rating', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||
ipcRenderer.on('request-seek', cb);
|
||||
const requestSeek = (cb: (data: { offset: number }) => void) => {
|
||||
ipcRenderer.on('request-seek', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('request-volume', cb);
|
||||
const requestVolume = (cb: (data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('request-volume', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const setRemoteEnabled = (enabled: boolean): Promise<null | string> => {
|
||||
|
||||
+72
-33
@@ -1,6 +1,6 @@
|
||||
import { ipcRenderer, IpcRendererEvent, webFrame } from 'electron';
|
||||
import { ipcRenderer, webFrame } from 'electron';
|
||||
|
||||
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '../main/utils';
|
||||
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '../main/env';
|
||||
|
||||
const openItem = async (path: string) => {
|
||||
return ipcRenderer.invoke('open-item', path);
|
||||
@@ -10,29 +10,44 @@ const openApplicationDirectory = async () => {
|
||||
return ipcRenderer.invoke('open-application-directory');
|
||||
};
|
||||
|
||||
const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => {
|
||||
ipcRenderer.on('player-error-listener', cb);
|
||||
const getCustomCss = async (): Promise<
|
||||
| undefined
|
||||
| {
|
||||
content: string;
|
||||
exists: boolean;
|
||||
path?: string;
|
||||
}
|
||||
> => {
|
||||
return ipcRenderer.invoke('custom-css-get');
|
||||
};
|
||||
|
||||
const saveCustomCss = async (content: string) => {
|
||||
return ipcRenderer.invoke('custom-css-save', { content });
|
||||
};
|
||||
|
||||
const openCustomCssFolder = async () => {
|
||||
return ipcRenderer.invoke('custom-css-open-folder');
|
||||
};
|
||||
|
||||
const customCssUpdatedListener = (
|
||||
cb: (data: { content?: string; exists?: boolean; path?: string }) => void,
|
||||
) => {
|
||||
const listener = (_event: unknown, data: { content?: string; exists?: boolean }) => cb(data);
|
||||
ipcRenderer.on('custom-css-updated', listener);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener('custom-css-updated', listener);
|
||||
};
|
||||
};
|
||||
|
||||
const playerErrorListener = (cb: (data: { code: number }) => void) => {
|
||||
ipcRenderer.on('player-error-listener', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const mainMessageListener = (
|
||||
cb: (
|
||||
event: IpcRendererEvent,
|
||||
data: { message: string; type: 'error' | 'info' | 'success' | 'warning' },
|
||||
) => void,
|
||||
cb: (data: { message: string; type: 'error' | 'info' | 'success' | 'warning' }) => void,
|
||||
) => {
|
||||
ipcRenderer.on('toast-from-main', cb);
|
||||
};
|
||||
|
||||
const logger = (
|
||||
cb: (
|
||||
event: IpcRendererEvent,
|
||||
data: {
|
||||
message: string;
|
||||
type: 'debug' | 'error' | 'info' | 'verbose' | 'warning';
|
||||
},
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.send('logger', cb);
|
||||
ipcRenderer.on('toast-from-main', (_, data) => cb(data));
|
||||
};
|
||||
|
||||
const download = (url: string) => {
|
||||
@@ -43,6 +58,14 @@ const checkForUpdates = (): Promise<{ updateAvailable: boolean; version?: string
|
||||
return ipcRenderer.invoke('app-check-for-updates');
|
||||
};
|
||||
|
||||
const startPowerSaveBlocker = (full: boolean) => {
|
||||
return ipcRenderer.invoke('power-save-blocker-start', { full });
|
||||
};
|
||||
|
||||
const stopPowerSaveBlocker = () => {
|
||||
return ipcRenderer.invoke('power-save-blocker-stop');
|
||||
};
|
||||
|
||||
const forceGarbageCollection = (): boolean => {
|
||||
try {
|
||||
if (typeof global.gc === 'function') {
|
||||
@@ -61,41 +84,51 @@ const forceGarbageCollection = (): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
const rendererOpenSettings = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-settings', cb);
|
||||
const setInputFocused = (focused: boolean) => {
|
||||
ipcRenderer.send('input-focus-state', focused);
|
||||
};
|
||||
|
||||
const rendererOpenCommandPalette = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-command-palette', cb);
|
||||
const rendererOpenSettings = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-open-settings', () => cb());
|
||||
};
|
||||
|
||||
const rendererOpenManageServers = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-manage-servers', cb);
|
||||
const rendererOpenCommandPalette = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-open-command-palette', () => cb());
|
||||
};
|
||||
|
||||
const rendererTogglePrivateMode = (cb: (event: IpcRendererEvent) => void) => {
|
||||
const rendererOpenManageServers = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-open-manage-servers', () => cb());
|
||||
};
|
||||
|
||||
const rendererTogglePrivateMode = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-toggle-private-mode', cb);
|
||||
};
|
||||
|
||||
const rendererToggleSidebar = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-toggle-sidebar', cb);
|
||||
const rendererToggleSidebar = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-toggle-sidebar', () => cb());
|
||||
};
|
||||
|
||||
const rendererOpenReleaseNotes = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-open-release-notes', cb);
|
||||
const rendererOpenReleaseNotes = (cb: () => void) => {
|
||||
ipcRenderer.on('renderer-open-release-notes', () => cb());
|
||||
};
|
||||
|
||||
const rendererUpdateAvailable = (cb: (version: string) => void) => {
|
||||
ipcRenderer.on('update-available', (_, version) => cb(version));
|
||||
};
|
||||
|
||||
export const utils = {
|
||||
checkForUpdates,
|
||||
customCssUpdatedListener,
|
||||
disableAutoUpdates,
|
||||
download,
|
||||
forceGarbageCollection,
|
||||
getCustomCss,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
isWindows,
|
||||
logger,
|
||||
mainMessageListener,
|
||||
openApplicationDirectory,
|
||||
openCustomCssFolder,
|
||||
openItem,
|
||||
playerErrorListener,
|
||||
rendererOpenCommandPalette,
|
||||
@@ -104,6 +137,12 @@ export const utils = {
|
||||
rendererOpenSettings,
|
||||
rendererTogglePrivateMode,
|
||||
rendererToggleSidebar,
|
||||
rendererUpdateAvailable,
|
||||
saveCustomCss,
|
||||
separator: isWindows() ? '\\' : '/',
|
||||
setInputFocused,
|
||||
startPowerSaveBlocker,
|
||||
stopPowerSaveBlocker,
|
||||
};
|
||||
|
||||
export type Utils = typeof utils;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
const setLocalSurfaceVisible = (visible: boolean) => {
|
||||
ipcRenderer.send('visualizer-set-local-surface-visible', visible);
|
||||
};
|
||||
|
||||
export const visualizer = {
|
||||
setLocalSurfaceVisible,
|
||||
};
|
||||
|
||||
export type VisualizerApi = typeof visualizer;
|
||||
+63
-183
@@ -34,10 +34,8 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
||||
|
||||
if (!serverType) {
|
||||
toast.error({
|
||||
message: i18n.t('error.serverNotSelectedError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
|
||||
message: i18n.t('error.serverNotSelectedError') as string,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
});
|
||||
throw new Error(`No server selected`);
|
||||
}
|
||||
@@ -47,13 +45,13 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
||||
if (typeof controllerFn !== 'function') {
|
||||
toast.error({
|
||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
i18n.t('error.endpointNotImplementedError', {
|
||||
endpoint,
|
||||
postProcess: 'sentenceCase',
|
||||
|
||||
serverType,
|
||||
}) as string,
|
||||
);
|
||||
@@ -92,9 +90,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: addToPlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: addToPlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -109,9 +105,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createFavorite`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: createFavorite`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -123,9 +117,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createInternetRadioStation`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: createInternetRadioStation`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -137,9 +129,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createPlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: createPlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -151,9 +141,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteArtistImage`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteArtistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -165,9 +153,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteFavorite`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteFavorite`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -179,9 +165,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteInternetRadioStation`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteInternetRadioStation`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -193,9 +177,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteInternetRadioStationImage`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deleteInternetRadioStationImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -207,9 +189,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deletePlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deletePlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -221,9 +201,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deletePlaylistImage`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: deletePlaylistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -235,9 +213,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumArtistDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumArtistDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -261,9 +237,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumArtistList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumArtistList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -281,9 +255,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumArtistListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumArtistListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -301,9 +273,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -315,9 +285,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumInfo`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumInfo`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -329,9 +297,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -349,9 +315,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -369,9 +333,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getAlbumRadio`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getAlbumRadio`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -383,9 +345,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getArtistList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -403,9 +363,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getArtistListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -423,9 +381,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistRadio`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getArtistRadio`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -437,9 +393,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getDownloadUrl`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getDownloadUrl`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -451,9 +405,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getFolder`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getFolder`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -471,9 +423,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getGenreList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getGenreList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -529,9 +479,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getInternetRadioStations`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getInternetRadioStations`);
|
||||
}
|
||||
return apiController(
|
||||
'getInternetRadioStations',
|
||||
@@ -542,9 +490,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getLyrics`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getLyrics`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -556,9 +502,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getMusicFolderList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getMusicFolderList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -570,9 +514,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -584,9 +526,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -598,9 +538,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -612,9 +550,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlaylistSongList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistSongList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -626,9 +562,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getPlayQueue`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getPlayQueue`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -640,9 +574,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getRandomSongList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getRandomSongList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -660,9 +592,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getRoles`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getRoles`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -674,9 +604,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getServerInfo`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getServerInfo`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -688,9 +616,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSimilarSongs`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSimilarSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -708,9 +634,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSongDetail`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSongDetail`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -722,9 +646,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSongList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSongList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -742,9 +664,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getSongListCount`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getSongListCount`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -762,9 +682,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getStreamUrl`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getStreamUrl`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -776,9 +694,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getStructuredLyrics`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getStructuredLyrics`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -790,9 +706,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getTags`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getTags`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -804,9 +718,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getTopSongs`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getTopSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -818,9 +730,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getUserInfo`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getUserInfo`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -832,9 +742,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getUserList`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getUserList`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -846,9 +754,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: movePlaylistItem`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: movePlaylistItem`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -860,9 +766,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: removeFromPlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: removeFromPlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -874,9 +778,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: replacePlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: replacePlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -888,9 +790,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: savePlayQueue`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: savePlayQueue`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -902,9 +802,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: scrobble`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: scrobble`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -916,9 +814,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: search`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: search`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -936,9 +832,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: setPlaylistSongs`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: setPlaylistSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -950,9 +844,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: setRating`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: setRating`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -964,9 +856,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: shareItem`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: shareItem`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -978,9 +868,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: updateInternetRadioStation`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: updateInternetRadioStation`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -992,9 +880,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: updatePlaylist`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: updatePlaylist`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -1006,9 +892,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadArtistImage`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: uploadArtistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -1020,9 +904,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadInternetRadioStationImage`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: uploadInternetRadioStationImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
@@ -1034,9 +916,7 @@ export const controller: GeneralController = {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadPlaylistImage`,
|
||||
);
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: uploadPlaylistImage`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
|
||||
@@ -54,6 +54,15 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
deleteArtistImage: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
path: 'Items/:id/Images/Primary',
|
||||
responses: {
|
||||
204: jfType._response.deleteArtistImage,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
deletePlaylist: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
@@ -63,6 +72,15 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
deletePlaylistImage: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
path: 'Items/:id/Images/Primary',
|
||||
responses: {
|
||||
204: jfType._response.deletePlaylistImage,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
getAlbumArtistDetail: {
|
||||
method: 'GET',
|
||||
path: 'users/:userId/items/:id',
|
||||
@@ -356,6 +374,24 @@ export const contract = c.router({
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
uploadArtistImage: {
|
||||
body: z.string(),
|
||||
method: 'POST',
|
||||
path: 'Items/:id/Images/Primary',
|
||||
responses: {
|
||||
204: jfType._response.uploadArtistImage,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
uploadPlaylistImage: {
|
||||
body: z.string(),
|
||||
method: 'POST',
|
||||
path: 'Items/:id/Images/Primary',
|
||||
responses: {
|
||||
204: jfType._response.uploadPlaylistImage,
|
||||
400: jfType._response.error,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const axiosClient = axios.create({});
|
||||
@@ -404,11 +440,12 @@ export const createAuthHeader = (): string => {
|
||||
};
|
||||
|
||||
export const jfApiClient = (args: {
|
||||
forceRemoteUrl?: boolean;
|
||||
server: null | ServerListItemWithCredential;
|
||||
signal?: AbortSignal;
|
||||
url?: string;
|
||||
}) => {
|
||||
const { server, signal, url } = args;
|
||||
const { forceRemoteUrl, server, signal, url } = args;
|
||||
|
||||
return initClient(contract, {
|
||||
api: async ({ body, headers, method, path }) => {
|
||||
@@ -418,7 +455,7 @@ export const jfApiClient = (args: {
|
||||
const { params, path: api } = parsePath(path);
|
||||
|
||||
if (server) {
|
||||
const serverUrl = getServerUrl(server);
|
||||
const serverUrl = getServerUrl(server, forceRemoteUrl);
|
||||
baseUrl = serverUrl;
|
||||
token = server?.credential;
|
||||
} else {
|
||||
@@ -447,11 +484,7 @@ export const jfApiClient = (args: {
|
||||
} catch (e: any | AxiosError | Error) {
|
||||
if (isAxiosError(e)) {
|
||||
if (e.code === 'ERR_NETWORK') {
|
||||
throw new Error(
|
||||
i18n.t('error.networkError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.networkError') as string);
|
||||
}
|
||||
|
||||
const error = e as AxiosError;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import { set } from 'idb-keyval';
|
||||
import chunk from 'lodash/chunk';
|
||||
import filter from 'lodash/filter';
|
||||
@@ -13,6 +14,10 @@ import { getFeatures, hasFeature, sortSongList, VersionInfo } from '/@/shared/ap
|
||||
import {
|
||||
albumArtistListSortMap,
|
||||
albumListSortMap,
|
||||
DeleteArtistImageArgs,
|
||||
DeleteArtistImageResponse,
|
||||
DeletePlaylistImageArgs,
|
||||
DeletePlaylistImageResponse,
|
||||
Folder,
|
||||
genreListSortMap,
|
||||
ImageArgs,
|
||||
@@ -29,6 +34,10 @@ import {
|
||||
SortOrder,
|
||||
sortOrderMap,
|
||||
Tag,
|
||||
UploadArtistImageArgs,
|
||||
UploadArtistImageResponse,
|
||||
UploadPlaylistImageArgs,
|
||||
UploadPlaylistImageResponse,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
@@ -63,6 +72,94 @@ const formatCommaDelimitedString = (value: string[]) => {
|
||||
return value.join(',');
|
||||
};
|
||||
|
||||
const getImageContentType = (bytes: Uint8Array): string => {
|
||||
if (bytes[0] === 0x89 && bytes[1] === 0x50) {
|
||||
return 'image/png';
|
||||
}
|
||||
if (bytes[0] === 0xff && bytes[1] === 0xd8) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
if (bytes[0] === 0x47 && bytes[1] === 0x49) {
|
||||
return 'image/gif';
|
||||
}
|
||||
if (bytes[0] === 0x52 && bytes[1] === 0x49) {
|
||||
return 'image/webp';
|
||||
}
|
||||
|
||||
return 'image/jpeg';
|
||||
};
|
||||
|
||||
const uint8ArrayToBase64 = (bytes: Uint8Array): string => {
|
||||
let binary = '';
|
||||
const chunkSize = 0x8000;
|
||||
|
||||
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||
const chunk = bytes.subarray(i, i + chunkSize);
|
||||
binary += String.fromCharCode(...chunk);
|
||||
}
|
||||
|
||||
return btoa(binary);
|
||||
};
|
||||
|
||||
type JellyfinApiClientProps = DeletePlaylistImageArgs['apiClientProps'];
|
||||
|
||||
const deleteItemPrimaryImage = async (
|
||||
apiClientProps: JellyfinApiClientProps,
|
||||
id: string,
|
||||
errorMessage: string,
|
||||
): Promise<boolean> => {
|
||||
const res = await jfApiClient({
|
||||
...apiClientProps,
|
||||
server: apiClientProps.server ?? null,
|
||||
}).deleteArtistImage({
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 204) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadItemPrimaryImage = async (
|
||||
apiClientProps: JellyfinApiClientProps,
|
||||
id: string,
|
||||
image: Uint8Array,
|
||||
errorMessage: string,
|
||||
): Promise<boolean> => {
|
||||
const server = apiClientProps.server;
|
||||
const serverUrl = getServerUrl(server);
|
||||
|
||||
if (!serverUrl) {
|
||||
throw new Error('Server is required');
|
||||
}
|
||||
|
||||
const contentType = getImageContentType(image);
|
||||
const base64 = uint8ArrayToBase64(image);
|
||||
|
||||
const authHeader = createAuthHeader();
|
||||
const authorization = server?.credential
|
||||
? authHeader.concat(`, Token="${server.credential}"`)
|
||||
: authHeader;
|
||||
|
||||
const res = await axios.post(`${serverUrl}/Items/${id}/Images/Primary`, base64, {
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': contentType,
|
||||
},
|
||||
signal: apiClientProps.signal,
|
||||
});
|
||||
|
||||
if (res.status !== 204) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Limit the query to 50 at a time to be *extremely* conservative on the
|
||||
// length of the full URL, since the ids are part of the query string and
|
||||
// not the POST body
|
||||
@@ -80,7 +177,14 @@ const VERSION_INFO: VersionInfo = [
|
||||
[ServerFeature.PUBLIC_PLAYLIST]: [1],
|
||||
},
|
||||
],
|
||||
['10.0.0', { [ServerFeature.TAGS]: [1] }],
|
||||
[
|
||||
'10.0.0',
|
||||
{
|
||||
[ServerFeature.ARTIST_IMAGE_UPLOAD]: [1],
|
||||
[ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1],
|
||||
[ServerFeature.TAGS]: [1],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const JF_FIELDS = {
|
||||
@@ -231,6 +335,11 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
id: res.body.Id,
|
||||
};
|
||||
},
|
||||
deleteArtistImage: async (args: DeleteArtistImageArgs): Promise<DeleteArtistImageResponse> => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
return deleteItemPrimaryImage(apiClientProps, query.id, 'Failed to delete artist image');
|
||||
},
|
||||
deleteFavorite: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -281,6 +390,13 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
deletePlaylistImage: async (
|
||||
args: DeletePlaylistImageArgs,
|
||||
): Promise<DeletePlaylistImageResponse> => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
return deleteItemPrimaryImage(apiClientProps, query.id, 'Failed to delete playlist image');
|
||||
},
|
||||
getAlbumArtistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -395,10 +511,15 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
userId: apiClientProps.server.userId,
|
||||
},
|
||||
query: {
|
||||
AlbumIds: query.id,
|
||||
EnableUserData: true,
|
||||
Fields: JF_FIELDS.SONG,
|
||||
IncludeItemTypes: 'Audio',
|
||||
ParentId: query.id,
|
||||
Recursive: true,
|
||||
SortBy: 'ParentIndexNumber,IndexNumber,SortName',
|
||||
SortOrder: JFSortOrder.ASC,
|
||||
StartIndex: 0,
|
||||
UserId: apiClientProps.server.userId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -406,10 +527,11 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get album detail');
|
||||
}
|
||||
|
||||
return jfNormalize.album(
|
||||
{ ...res.body, Songs: songsRes.body.Items },
|
||||
apiClientProps.server,
|
||||
);
|
||||
// Workaround for Jellyfin bug that returns items that share the same album name
|
||||
const albumIdSet = new Set([query.id]);
|
||||
const songs = songsRes.body.Items.filter((item) => albumIdSet.has(item.AlbumId!));
|
||||
|
||||
return jfNormalize.album({ ...res.body, Songs: songs }, apiClientProps.server);
|
||||
},
|
||||
getAlbumList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -503,14 +625,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get album radio songs');
|
||||
}
|
||||
|
||||
return res.body.Items.map((song) =>
|
||||
jfNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
return res.body.Items.map((song) => jfNormalize.song(song, apiClientProps.server));
|
||||
},
|
||||
getArtistList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -566,21 +681,15 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
return res.body.Items.map((song) =>
|
||||
jfNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
return res.body.Items.map((song) => jfNormalize.song(song, apiClientProps.server));
|
||||
},
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
|
||||
},
|
||||
getFolder: async ({ apiClientProps, query }) => {
|
||||
getFolder: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
const userId = apiClientProps.server?.userId;
|
||||
|
||||
if (!userId) throw new Error('No userId found');
|
||||
@@ -923,6 +1032,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
Fields: JF_FIELDS.PLAYLIST_LIST,
|
||||
IncludeItemTypes: 'Playlist',
|
||||
Limit: query.limit,
|
||||
MediaTypes: 'Audio, Unknown',
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: playlistListSortMap.jellyfin[query.sortBy],
|
||||
@@ -969,14 +1079,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.Items.map((item) =>
|
||||
jfNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.TotalRecordCount,
|
||||
};
|
||||
@@ -1029,14 +1132,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.Items.map((item) =>
|
||||
jfNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.Items.length || 0,
|
||||
};
|
||||
@@ -1051,7 +1147,9 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get server info');
|
||||
}
|
||||
|
||||
const defaultFeatures = {};
|
||||
const defaultFeatures = {
|
||||
[ServerFeature.REPORT_PLAYBACK]: [1],
|
||||
};
|
||||
|
||||
const features = {
|
||||
...defaultFeatures,
|
||||
@@ -1086,14 +1184,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
if (res.status === 200 && res.body.Items.length) {
|
||||
const results = res.body.Items.reduce<Song[]>((acc, song) => {
|
||||
if (song.Id !== query.songId) {
|
||||
acc.push(
|
||||
jfNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
acc.push(jfNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -1122,14 +1213,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return mix.body.Items.reduce<Song[]>((acc, song) => {
|
||||
if (song.Id !== query.songId) {
|
||||
acc.push(
|
||||
jfNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
acc.push(jfNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -1149,12 +1233,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get song detail');
|
||||
}
|
||||
|
||||
return jfNormalize.song(
|
||||
res.body,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
);
|
||||
return jfNormalize.song(res.body, apiClientProps.server);
|
||||
},
|
||||
getSongList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -1266,14 +1345,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: items.map((item) =>
|
||||
jfNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
items: items.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount,
|
||||
};
|
||||
@@ -1405,14 +1477,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get top song list');
|
||||
}
|
||||
|
||||
const items = res.body.Items.map((item) =>
|
||||
jfNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
const items = res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server));
|
||||
|
||||
if (type === 'personal') {
|
||||
const sorted = orderBy(
|
||||
@@ -1514,12 +1579,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
const existingSongs = existingSongsRes.body.Items.map((item) =>
|
||||
jfNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
jfNormalize.song(item, apiClientProps.server),
|
||||
);
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
@@ -1660,6 +1720,17 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (query.event === 'stop') {
|
||||
jfApiClient(apiClientProps).scrobbleStopped({
|
||||
body: {
|
||||
ItemId: query.id,
|
||||
PositionTicks: position,
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
jfApiClient(apiClientProps).scrobbleProgress({
|
||||
body: {
|
||||
ItemId: query.id,
|
||||
@@ -1759,14 +1830,7 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
jfNormalize.albumArtist(item, apiClientProps.server),
|
||||
),
|
||||
albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)),
|
||||
songs: songs.map((item) =>
|
||||
jfNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server)),
|
||||
};
|
||||
},
|
||||
setPlaylistSongs: async (args) => {
|
||||
@@ -1830,6 +1894,28 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
uploadArtistImage: async (args: UploadArtistImageArgs): Promise<UploadArtistImageResponse> => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
return uploadItemPrimaryImage(
|
||||
apiClientProps,
|
||||
query.id,
|
||||
body.image,
|
||||
'Failed to upload artist image',
|
||||
);
|
||||
},
|
||||
uploadPlaylistImage: async (
|
||||
args: UploadPlaylistImageArgs,
|
||||
): Promise<UploadPlaylistImageResponse> => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
return uploadItemPrimaryImage(
|
||||
apiClientProps,
|
||||
query.id,
|
||||
body.image,
|
||||
'Failed to upload playlist image',
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function getLibraryId(musicFolderId?: string | string[]) {
|
||||
|
||||
@@ -405,12 +405,8 @@ axiosClient.interceptors.response.use(
|
||||
|
||||
if (res.status === 429) {
|
||||
toast.error({
|
||||
message: i18n.t('error.loginRateError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
title: i18n.t('error.sessionExpiredError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
message: i18n.t('error.loginRateError') as string,
|
||||
title: i18n.t('error.sessionExpiredError') as string,
|
||||
});
|
||||
|
||||
const serverId = currentServer.id;
|
||||
@@ -425,11 +421,7 @@ axiosClient.interceptors.response.use(
|
||||
throw TIMEOUT_ERROR;
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
i18n.t('error.authenticatedFailed', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.authenticatedFailed') as string);
|
||||
}
|
||||
|
||||
const newCredential = res.data.token;
|
||||
@@ -479,11 +471,12 @@ axiosClient.interceptors.response.use(
|
||||
);
|
||||
|
||||
export const ndApiClient = (args: {
|
||||
forceRemoteUrl?: boolean;
|
||||
server: null | ServerListItemWithCredential;
|
||||
signal?: AbortSignal;
|
||||
url?: string;
|
||||
}) => {
|
||||
const { server, signal, url } = args;
|
||||
const { forceRemoteUrl, server, signal, url } = args;
|
||||
|
||||
return initClient(contract, {
|
||||
api: async ({ body, headers, method, path }) => {
|
||||
@@ -493,7 +486,7 @@ export const ndApiClient = (args: {
|
||||
const { params, path: api } = parsePath(path);
|
||||
|
||||
if (server) {
|
||||
const serverUrl = getServerUrl(server);
|
||||
const serverUrl = getServerUrl(server, forceRemoteUrl);
|
||||
baseUrl = serverUrl ? `${serverUrl}/api` : undefined;
|
||||
token = server?.ndCredential;
|
||||
} else {
|
||||
@@ -522,11 +515,7 @@ export const ndApiClient = (args: {
|
||||
} catch (e: any | AxiosError | Error) {
|
||||
if (isAxiosError(e)) {
|
||||
if (e.code === 'ERR_NETWORK') {
|
||||
throw new Error(
|
||||
i18n.t('error.networkError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.networkError') as string);
|
||||
}
|
||||
|
||||
const error = e as AxiosError;
|
||||
|
||||
@@ -100,9 +100,6 @@ const EXCLUDED_SONG_TAGS = new Set<string>(['disctotal', 'tracktotal']);
|
||||
const numericSortCollator = new Intl.Collator(undefined, { numeric: true });
|
||||
const collator = new Intl.Collator();
|
||||
|
||||
// Tags that use IDs as values as opposed to the tag value
|
||||
const ID_TAGS = new Set<string>(['albumversion', 'mood']);
|
||||
|
||||
const excludeMissing = (server?: null | ServerListItemWithCredential) => {
|
||||
if (!server) {
|
||||
return undefined;
|
||||
@@ -370,7 +367,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getAlbumDetail: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
|
||||
params: {
|
||||
@@ -396,8 +393,6 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
return ndNormalize.album(
|
||||
{ ...albumRes.body.data, songs: songsData.body.data },
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
);
|
||||
},
|
||||
getAlbumInfo: async (args) => {
|
||||
@@ -421,7 +416,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
};
|
||||
},
|
||||
getAlbumList: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const genres = hasFeature(apiClientProps.server, ServerFeature.BFR)
|
||||
? query.genreIds
|
||||
@@ -456,14 +451,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((album) =>
|
||||
ndNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
@@ -716,14 +704,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((item) =>
|
||||
ndNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)),
|
||||
startIndex: 0,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
@@ -740,14 +721,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
const { changedBy, current, items = [], position, updatedAt } = res.body.data; // if there is no queue saved, items is undefined
|
||||
|
||||
const entries = items.map((song) =>
|
||||
ndNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
const entries = items.map((song) => ndNormalize.song(song, apiClientProps.server));
|
||||
|
||||
return {
|
||||
changed: updatedAt,
|
||||
@@ -839,12 +813,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get song detail');
|
||||
}
|
||||
|
||||
return ndNormalize.song(
|
||||
res.body.data,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
);
|
||||
return ndNormalize.song(res.body.data, apiClientProps.server);
|
||||
},
|
||||
getSongList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -884,14 +853,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((song) =>
|
||||
ndNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server)),
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
};
|
||||
@@ -948,13 +910,13 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
if (!EXCLUDED_TAGS.has(tag.tagName)) {
|
||||
if (tagsToValues.has(tag.tagName)) {
|
||||
tagsToValues.get(tag.tagName)!.push({
|
||||
id: ID_TAGS.has(tag.tagName) ? tag.id : tag.tagValue,
|
||||
id: tag.id,
|
||||
name: tag.tagValue,
|
||||
});
|
||||
} else {
|
||||
tagsToValues.set(tag.tagName, [
|
||||
{
|
||||
id: ID_TAGS.has(tag.tagName) ? tag.id : tag.tagValue,
|
||||
id: tag.id,
|
||||
name: tag.tagValue,
|
||||
},
|
||||
]);
|
||||
@@ -1008,12 +970,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return {
|
||||
items: (res.body.topSongs?.song || []).map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||
@@ -1123,12 +1080,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
const existingSongs = existingSongsRes.body.data.map((item) =>
|
||||
ndNormalize.song(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
ndNormalize.song(item, apiClientProps.server),
|
||||
);
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { initClient, initContract } from '@ts-rest/core';
|
||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
|
||||
import omitBy from 'lodash/omitBy';
|
||||
import qs from 'qs';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -290,6 +289,14 @@ export const contract = c.router({
|
||||
200: ssType._response.removeFavorite,
|
||||
},
|
||||
},
|
||||
reportPlayback: {
|
||||
method: 'GET',
|
||||
path: 'reportPlayback.view',
|
||||
query: ssType._parameters.reportPlayback,
|
||||
responses: {
|
||||
200: ssType._response.reportPlayback,
|
||||
},
|
||||
},
|
||||
savePlayQueue: {
|
||||
method: 'GET',
|
||||
path: 'savePlayQueue.view',
|
||||
@@ -362,7 +369,7 @@ axiosClient.interceptors.response.use(
|
||||
if (data['subsonic-response'].error.code !== 0) {
|
||||
toast.error({
|
||||
message: data['subsonic-response'].error.message,
|
||||
title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||
title: i18n.t('error.genericError') as string,
|
||||
});
|
||||
|
||||
// Since we do status === 200, override this value with the error code
|
||||
@@ -377,11 +384,39 @@ axiosClient.interceptors.response.use(
|
||||
},
|
||||
);
|
||||
|
||||
const keysToSkipEmptyCheck = new Set([
|
||||
'artist',
|
||||
'comment',
|
||||
'genre',
|
||||
'name',
|
||||
'query',
|
||||
'u',
|
||||
'username',
|
||||
]);
|
||||
|
||||
const parsePath = (fullPath: string) => {
|
||||
const [path, params] = fullPath.split('?');
|
||||
|
||||
const parsedParams = qs.parse(params, { arrayLimit: 99999, parameterLimit: 99999 });
|
||||
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
|
||||
const url = new URLSearchParams(params);
|
||||
const notNilParams: Record<string, string[]> = {};
|
||||
|
||||
for (const [key, value] of url) {
|
||||
if (!keysToSkipEmptyCheck.has(key) && (value === 'undefined' || value === 'null')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let realKey = key;
|
||||
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
realKey = key.split('[')[0];
|
||||
}
|
||||
|
||||
if (realKey in notNilParams) {
|
||||
notNilParams[realKey].push(value);
|
||||
} else {
|
||||
notNilParams[realKey] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
params: notNilParams,
|
||||
@@ -401,12 +436,13 @@ const silentlyTransformResponse = (data: any) => {
|
||||
};
|
||||
|
||||
export const ssApiClient = (args: {
|
||||
forceRemoteUrl?: boolean;
|
||||
server: null | ServerListItemWithCredential;
|
||||
signal?: AbortSignal;
|
||||
silent?: boolean;
|
||||
url?: string;
|
||||
}) => {
|
||||
const { server, signal, silent, url } = args;
|
||||
const { forceRemoteUrl, server, signal, silent, url } = args;
|
||||
|
||||
return initClient(contract, {
|
||||
api: async ({ body, headers, method, path, rawQuery }) => {
|
||||
@@ -416,7 +452,7 @@ export const ssApiClient = (args: {
|
||||
const { params, path: api } = parsePath(path);
|
||||
|
||||
if (server) {
|
||||
const serverUrl = getServerUrl(server);
|
||||
const serverUrl = getServerUrl(server, forceRemoteUrl);
|
||||
baseUrl = serverUrl ? `${serverUrl}/rest` : undefined;
|
||||
const token = server.credential;
|
||||
const params = token.split(/&?\w=/gm);
|
||||
@@ -496,11 +532,7 @@ export const ssApiClient = (args: {
|
||||
} catch (e: any | AxiosError | Error) {
|
||||
if (isAxiosError(e)) {
|
||||
if (e.code === 'ERR_NETWORK') {
|
||||
throw new Error(
|
||||
i18n.t('error.networkError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}) as string,
|
||||
);
|
||||
throw new Error(i18n.t('error.networkError') as string);
|
||||
}
|
||||
|
||||
const error = e as AxiosError;
|
||||
|
||||
@@ -366,7 +366,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||
? query.id
|
||||
: undefined,
|
||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||
id:
|
||||
query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
|
||||
? query.id
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -419,7 +422,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||
? query.id
|
||||
: undefined,
|
||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||
id:
|
||||
query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
|
||||
? query.id
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -476,14 +482,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return {
|
||||
...ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
albums: artist.album?.map((album) =>
|
||||
ssNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||
similarArtists: null,
|
||||
};
|
||||
},
|
||||
@@ -558,7 +557,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
getAlbumArtistListCount: (args) =>
|
||||
SubsonicController.getAlbumArtistList({
|
||||
...args,
|
||||
context: args.context,
|
||||
query: { ...args.query, startIndex: 0 },
|
||||
}).then((res) => res!.totalRecordCount!),
|
||||
getAlbumDetail: async (args) => {
|
||||
@@ -574,12 +572,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get album detail');
|
||||
}
|
||||
|
||||
return ssNormalize.album(
|
||||
res.body.album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
);
|
||||
return ssNormalize.album(res.body.album, apiClientProps.server);
|
||||
},
|
||||
getAlbumList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
@@ -604,12 +597,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const results =
|
||||
res.body.searchResult3?.album?.map((album) =>
|
||||
ssNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
return {
|
||||
@@ -644,14 +632,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return artist.body.artist.album ?? [];
|
||||
});
|
||||
|
||||
const items = albums.map((album) =>
|
||||
ssNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));
|
||||
|
||||
return {
|
||||
items: sortAlbumList(items, query.sortBy, query.sortOrder),
|
||||
@@ -673,12 +654,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const allResults =
|
||||
res.body.starred?.album?.map((album) =>
|
||||
ssNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
return sortAndPaginate(allResults, {
|
||||
@@ -743,12 +719,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return {
|
||||
items:
|
||||
res.body.albumList2.album?.map((album) =>
|
||||
ssNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
) || [],
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
@@ -899,7 +870,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return totalRecordCount;
|
||||
},
|
||||
getAlbumRadio: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getSimilarSongs({
|
||||
query: {
|
||||
@@ -917,12 +888,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return res.body.similarSongs.song.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
},
|
||||
getArtistList: async (args) => {
|
||||
@@ -968,11 +934,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
getArtistListCount: async (args) =>
|
||||
SubsonicController.getArtistList({
|
||||
...args,
|
||||
context: args.context,
|
||||
query: { ...args.query, startIndex: 0 },
|
||||
}).then((res) => res!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getSimilarSongs2({
|
||||
query: {
|
||||
@@ -990,12 +955,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return res.body.similarSongs2.song.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
},
|
||||
getDownloadUrl: (args) => {
|
||||
@@ -1009,7 +969,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
'&c=Feishin'
|
||||
);
|
||||
},
|
||||
getFolder: async ({ apiClientProps, context, query }) => {
|
||||
getFolder: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';
|
||||
|
||||
const isRootFolderId = query.id === '0';
|
||||
@@ -1042,14 +1002,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
});
|
||||
}
|
||||
|
||||
let folders = items.map((item) =>
|
||||
ssNormalize.folder(
|
||||
item,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
let folders = items.map((item) => ssNormalize.folder(item, apiClientProps.server));
|
||||
|
||||
folders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]);
|
||||
|
||||
@@ -1077,12 +1030,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get folder');
|
||||
}
|
||||
|
||||
const folder = ssNormalize.folder(
|
||||
directoryRes.body.directory,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
);
|
||||
const folder = ssNormalize.folder(directoryRes.body.directory, apiClientProps.server);
|
||||
|
||||
let filteredFolders = folder.children?.folders || [];
|
||||
let filteredSongs = folder.children?.songs || [];
|
||||
@@ -1275,7 +1223,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return results.length;
|
||||
},
|
||||
getPlaylistSongList: async ({ apiClientProps, context, query }) => {
|
||||
getPlaylistSongList: async ({ apiClientProps, query }) => {
|
||||
const res = await ssApiClient(apiClientProps).getPlaylist({
|
||||
query: {
|
||||
id: query.id,
|
||||
@@ -1288,13 +1236,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const items =
|
||||
res.body.playlist.entry?.map((song, index) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
index,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server, index),
|
||||
) || [];
|
||||
|
||||
return {
|
||||
@@ -1303,7 +1245,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
totalRecordCount: items.length,
|
||||
};
|
||||
},
|
||||
getPlayQueue: async ({ apiClientProps, context }) => {
|
||||
getPlayQueue: async ({ apiClientProps }) => {
|
||||
if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
|
||||
const res = await ssApiClient(apiClientProps).getPlayQueueByIndex();
|
||||
|
||||
@@ -1318,15 +1260,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
changed: changed ?? '',
|
||||
changedBy: changedBy ?? '',
|
||||
currentIndex: currentIndex ?? 0,
|
||||
entry:
|
||||
entry?.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
) || [],
|
||||
entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
positionMs: position ?? 0,
|
||||
username: username ?? '',
|
||||
};
|
||||
@@ -1343,22 +1277,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
changed,
|
||||
changedBy,
|
||||
currentIndex: current ? entry.findIndex((item) => item.id === current) : 0,
|
||||
entry:
|
||||
entry?.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
) || [],
|
||||
entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
positionMs: position ?? 0,
|
||||
username,
|
||||
};
|
||||
}
|
||||
},
|
||||
getRandomSongList: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getRandomSongList({
|
||||
query: {
|
||||
@@ -1376,12 +1302,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const results = res.body.randomSongs?.song || [];
|
||||
const normalizedResults = results.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -1460,10 +1381,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
features.serverPlayQueue = [1];
|
||||
}
|
||||
|
||||
if (subsonicFeatures[SubsonicExtensions.PLAYBACK_REPORT]) {
|
||||
features.reportPlayback = [1];
|
||||
}
|
||||
|
||||
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
|
||||
},
|
||||
getSimilarSongs: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getSimilarSongs({
|
||||
query: {
|
||||
@@ -1482,21 +1407,14 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
|
||||
if (song.id !== query.songId) {
|
||||
acc.push(
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
);
|
||||
acc.push(ssNormalize.song(song, apiClientProps.server));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
getSongDetail: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getSong({
|
||||
query: {
|
||||
@@ -1508,14 +1426,9 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
throw new Error('Failed to get song detail');
|
||||
}
|
||||
|
||||
return ssNormalize.song(
|
||||
res.body.song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
);
|
||||
return ssNormalize.song(res.body.song, apiClientProps.server);
|
||||
},
|
||||
getSongList: async ({ apiClientProps, context, query }) => {
|
||||
getSongList: async ({ apiClientProps, query }) => {
|
||||
const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
|
||||
const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
|
||||
|
||||
@@ -1540,12 +1453,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return {
|
||||
items:
|
||||
res.body.searchResult3?.song?.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [],
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
@@ -1569,15 +1477,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
const results = res.body.songsByGenre?.song || [];
|
||||
|
||||
return {
|
||||
items:
|
||||
results.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
) || [],
|
||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
startIndex: 0,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
@@ -1596,12 +1496,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
let allResults =
|
||||
(res.body.starred?.song || []).map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
const filterArtistIds = query.albumArtistIds || query.artistIds;
|
||||
@@ -1686,15 +1581,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return {
|
||||
items:
|
||||
results.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
) || [],
|
||||
items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
|
||||
startIndex: 0,
|
||||
totalRecordCount: results.length,
|
||||
};
|
||||
@@ -1720,12 +1607,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return {
|
||||
items:
|
||||
res.body.searchResult3?.song?.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [],
|
||||
startIndex: 0,
|
||||
totalRecordCount: null,
|
||||
@@ -2093,7 +1975,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
});
|
||||
},
|
||||
getTopSongs: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const type = query.type === 'personal' ? 'personal' : 'community';
|
||||
|
||||
@@ -2111,12 +1993,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return {
|
||||
items: (res.body.topSongs?.song || []).map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||
@@ -2179,7 +2056,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, context, query } = args;
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
// 1. Fetch existing songs from the playlist
|
||||
const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({
|
||||
@@ -2194,12 +2071,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const existingSongs =
|
||||
existingSongsRes.body.playlist.entry?.map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
@@ -2297,6 +2169,72 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
scrobble: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
if (hasFeature(apiClientProps.server, ServerFeature.REPORT_PLAYBACK)) {
|
||||
if (query.submission || query.event === 'start') {
|
||||
const res = await ssApiClient(apiClientProps).scrobble({
|
||||
query: {
|
||||
id: query.id,
|
||||
submission: query.submission,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to scrobble');
|
||||
}
|
||||
|
||||
if (query.submission) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultParams = {
|
||||
ignoreScrobble: true,
|
||||
mediaId: query.id,
|
||||
mediaType: query.mediaType,
|
||||
playbackRate: query.playbackRate,
|
||||
positionMs: query.position ?? 0,
|
||||
};
|
||||
|
||||
const reportPlayback = (state: 'paused' | 'playing' | 'starting' | 'stopped') => {
|
||||
return ssApiClient(apiClientProps).reportPlayback({
|
||||
query: {
|
||||
...defaultParams,
|
||||
state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
switch (query.event) {
|
||||
case 'pause':
|
||||
promises.push(reportPlayback('paused'));
|
||||
break;
|
||||
case 'start':
|
||||
promises.push(reportPlayback('starting'));
|
||||
promises.push(reportPlayback('playing'));
|
||||
break;
|
||||
case 'stop':
|
||||
promises.push(reportPlayback('stopped'));
|
||||
break;
|
||||
case 'unpause':
|
||||
promises.push(reportPlayback('playing'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for (const promise of promises) {
|
||||
const res = await promise;
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to report playback');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await ssApiClient(apiClientProps).scrobble({
|
||||
query: {
|
||||
id: query.id,
|
||||
@@ -2311,7 +2249,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return null;
|
||||
},
|
||||
search: async (args) => {
|
||||
const { apiClientProps, context, query } = args;
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).search3({
|
||||
query: {
|
||||
@@ -2335,20 +2273,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
),
|
||||
albums: (res.body.searchResult3?.album || []).map((album) =>
|
||||
ssNormalize.album(
|
||||
album,
|
||||
apiClientProps.server,
|
||||
args.context?.pathReplace,
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.album(album, apiClientProps.server),
|
||||
),
|
||||
songs: (res.body.searchResult3?.song || []).map((song) =>
|
||||
ssNormalize.song(
|
||||
song,
|
||||
apiClientProps.server,
|
||||
context?.pathReplace,
|
||||
context?.pathReplaceWith,
|
||||
),
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
+115
-8
@@ -15,19 +15,19 @@ import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
||||
import { useNativeMenuSync } from '/@/renderer/hooks/use-native-menu-sync';
|
||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||
import { AppRouter } from '/@/renderer/router/app-router';
|
||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
||||
import {
|
||||
useCssSettings,
|
||||
useHotkeySettings,
|
||||
useLanguage,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||
import { WebAudio } from '/@/shared/types/types';
|
||||
import '/@/shared/styles/global.css';
|
||||
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
||||
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
||||
|
||||
const ReleaseNotesModal = lazy(() =>
|
||||
import('./release-notes-modal').then((module) => ({
|
||||
default: module.ReleaseNotesModal,
|
||||
})),
|
||||
);
|
||||
import { ReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
||||
|
||||
const UpdateAvailableDialog = lazy(() =>
|
||||
import('./update-available-dialog').then((module) => ({
|
||||
@@ -36,6 +36,7 @@ const UpdateAvailableDialog = lazy(() =>
|
||||
);
|
||||
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
|
||||
export const App = () => {
|
||||
return <ThemedApp />;
|
||||
@@ -82,8 +83,8 @@ const AppShell = memo(function AppShell() {
|
||||
<AppRouter />
|
||||
</PlayerProvider>
|
||||
</WebAudioContext.Provider>
|
||||
<ReleaseNotesModal />
|
||||
<Suspense fallback={null}>
|
||||
<ReleaseNotesModal />
|
||||
<UpdateAvailableDialog />
|
||||
</Suspense>
|
||||
</>
|
||||
@@ -94,10 +95,12 @@ const AppEffects = () => (
|
||||
<>
|
||||
<SyncSettingsEffect />
|
||||
<UpdateCheckEffect />
|
||||
<CustomCssFileEffect />
|
||||
<CssSettingsEffect />
|
||||
<GlobalShortcutsEffect />
|
||||
<LanguageEffect />
|
||||
<NativeMenuSyncEffect />
|
||||
<InputFocusEffect />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -146,6 +149,71 @@ const CssSettingsEffect = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const CustomCssFileEffect = () => {
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const { content } = useCssSettings();
|
||||
const latestContentRef = useRef(content);
|
||||
|
||||
useEffect(() => {
|
||||
latestContentRef.current = content;
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron() || !utils) return;
|
||||
|
||||
let disposed = false;
|
||||
|
||||
const applyContent = (rawContent: string | undefined) => {
|
||||
const sanitized = sanitizeCss(`<style>${rawContent ?? ''}`);
|
||||
if (sanitized !== latestContentRef.current) {
|
||||
setSettings({
|
||||
css: {
|
||||
content: sanitized,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadCustomCss = async () => {
|
||||
try {
|
||||
const result = await utils.getCustomCss();
|
||||
|
||||
if (disposed || !result) return;
|
||||
|
||||
if (!result.exists && latestContentRef.current) {
|
||||
await utils.saveCustomCss(latestContentRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
applyContent(result.content);
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom css', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomCssUpdated = (data: { content?: string; exists?: boolean }) => {
|
||||
if (disposed) return;
|
||||
if (data?.exists === false) {
|
||||
applyContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
applyContent(data?.content);
|
||||
};
|
||||
|
||||
const removeCustomCssUpdatedListener =
|
||||
utils.customCssUpdatedListener(handleCustomCssUpdated);
|
||||
loadCustomCss();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
removeCustomCssUpdatedListener();
|
||||
};
|
||||
}, [setSettings]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const GlobalShortcutsEffect = () => {
|
||||
const { bindings } = useHotkeySettings();
|
||||
|
||||
@@ -175,3 +243,42 @@ const NativeMenuSyncEffect = () => {
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const InputFocusEffect = () => {
|
||||
useEffect(() => {
|
||||
if (!isElectron()) return;
|
||||
|
||||
const handleFocusIn = (e: FocusEvent) => {
|
||||
const target = e.target as Element | null;
|
||||
if (
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
(target instanceof HTMLElement && target.isContentEditable)
|
||||
) {
|
||||
window.api?.utils?.setInputFocused?.(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocusOut = (e: FocusEvent) => {
|
||||
const related = e.relatedTarget as Element | null;
|
||||
if (
|
||||
related instanceof HTMLInputElement ||
|
||||
related instanceof HTMLTextAreaElement ||
|
||||
(related instanceof HTMLElement && related.isContentEditable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
window.api?.utils?.setInputFocused?.(false);
|
||||
};
|
||||
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
document.addEventListener('focusout', handleFocusOut);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('focusin', handleFocusIn);
|
||||
document.removeEventListener('focusout', handleFocusOut);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import styles from './drag-preview.module.css';
|
||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { DragData } from '/@/shared/types/drag-and-drop';
|
||||
import { DragData, DragTarget } from '/@/shared/types/drag-and-drop';
|
||||
|
||||
interface DragPreviewProps {
|
||||
data: DragData;
|
||||
@@ -29,7 +29,8 @@ export const DragPreview = memo(({ data }: DragPreviewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const itemCount = items.length;
|
||||
const firstItem = items[0];
|
||||
const itemName = firstItem ? getItemName(firstItem) : 'Item';
|
||||
const folderName = data.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER ? data.id[0] : undefined;
|
||||
const itemName = folderName || (firstItem ? getItemName(firstItem) : 'Item');
|
||||
|
||||
const itemImage = useItemImageUrl({
|
||||
id: (firstItem as { imageId: string })?.imageId,
|
||||
@@ -50,6 +51,9 @@ export const DragPreview = memo(({ data }: DragPreviewProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles['icon-container']}>
|
||||
{data.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER && (
|
||||
<Icon icon="folder" size="xl" />
|
||||
)}
|
||||
{data.itemType === LibraryItem.ALBUM && <Icon icon="album" size="xl" />}
|
||||
{data.itemType === LibraryItem.SONG && (
|
||||
<Icon icon="itemSong" size="xl" />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const ROW_PLAY_CONTROL_COLUMN_IDS = [TableColumn.TRACK_NUMBER, TableColumn.ROW_INDEX] as const;
|
||||
|
||||
type RowPlayControlColumnId = (typeof ROW_PLAY_CONTROL_COLUMN_IDS)[number];
|
||||
|
||||
const isRowPlayControlColumnId = (columnId: TableColumn): columnId is RowPlayControlColumnId =>
|
||||
ROW_PLAY_CONTROL_COLUMN_IDS.includes(columnId as RowPlayControlColumnId);
|
||||
|
||||
export const getRowPlayControlColumnId = (
|
||||
columns: Array<Pick<ItemTableListColumnConfig, 'id'>>,
|
||||
): null | TableColumn => {
|
||||
for (const column of columns) {
|
||||
if (isRowPlayControlColumnId(column.id)) {
|
||||
return column.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isRowPlayControlColumn = (
|
||||
columnId: TableColumn,
|
||||
columns: Array<Pick<ItemTableListColumnConfig, 'id'>>,
|
||||
): boolean => getRowPlayControlColumnId(columns) === columnId;
|
||||
@@ -13,7 +13,7 @@ import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
||||
import { getListRefreshMutationKey } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { LibraryItem, SortKeyRandom } from '/@/shared/types/domain-types';
|
||||
|
||||
export const getListQueryKeyName = (itemType: LibraryItem): string => {
|
||||
switch (itemType) {
|
||||
@@ -108,8 +108,19 @@ export const useItemListInfiniteLoader = ({
|
||||
[serverId, itemType, query],
|
||||
);
|
||||
|
||||
const isRandomSort = query?.sortBy === SortKeyRandom;
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (pageNumber: number) => {
|
||||
if (isRandomSort) {
|
||||
const existingData =
|
||||
queryClient.getQueryData<InfiniteLoaderCacheData>(dataQueryKey);
|
||||
if (existingData?.pagesLoaded?.[pageNumber]) {
|
||||
lastFetchedPageRef.current = Math.max(lastFetchedPageRef.current, pageNumber);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const startIndex = pageNumber * itemsPerPage;
|
||||
const queryParams = {
|
||||
limit: itemsPerPage,
|
||||
@@ -118,6 +129,7 @@ export const useItemListInfiniteLoader = ({
|
||||
};
|
||||
|
||||
const result = await queryClient.fetchQuery({
|
||||
gcTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15,
|
||||
queryFn: async ({ signal }) => {
|
||||
const result = await listQueryFn({
|
||||
apiClientProps: { serverId, signal },
|
||||
@@ -127,6 +139,7 @@ export const useItemListInfiniteLoader = ({
|
||||
return result;
|
||||
},
|
||||
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId, queryParams),
|
||||
staleTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15,
|
||||
});
|
||||
|
||||
// Update the query data with the fetched page
|
||||
@@ -154,13 +167,32 @@ export const useItemListInfiniteLoader = ({
|
||||
// Track the last fetched page
|
||||
lastFetchedPageRef.current = Math.max(lastFetchedPageRef.current, pageNumber);
|
||||
},
|
||||
[itemsPerPage, query, queryClient, serverId, dataQueryKey, listQueryFn, itemType],
|
||||
[
|
||||
itemsPerPage,
|
||||
query,
|
||||
queryClient,
|
||||
serverId,
|
||||
dataQueryKey,
|
||||
listQueryFn,
|
||||
itemType,
|
||||
isRandomSort,
|
||||
],
|
||||
);
|
||||
|
||||
// Reset the loaded pages and refetch current page when the query changes
|
||||
useEffect(() => {
|
||||
const currentDataQueryKey = JSON.stringify(dataQueryKey);
|
||||
|
||||
if (isRandomSort) {
|
||||
const existingData = queryClient.getQueryData<InfiniteLoaderCacheData | undefined>(
|
||||
dataQueryKey,
|
||||
);
|
||||
if (existingData?.dataMap && existingData.dataMap.size > 0) {
|
||||
previousDataQueryKeyRef.current = currentDataQueryKey;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (previousDataQueryKeyRef.current === currentDataQueryKey || isRefetchingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
|
||||
import { getListRefreshMutationKey } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { LibraryItem, SortKeyRandom } from '/@/shared/types/domain-types';
|
||||
|
||||
const getQueryKeyName = (itemType: LibraryItem): string => {
|
||||
switch (itemType) {
|
||||
@@ -76,6 +76,8 @@ export const useItemListPaginatedLoader = ({
|
||||
const fetchRange = getFetchRange(currentPage, itemsPerPage);
|
||||
const startIndex = fetchRange.startIndex;
|
||||
|
||||
const isRandomSort = query?.sortBy === SortKeyRandom;
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
limit: itemsPerPage,
|
||||
@@ -86,7 +88,7 @@ export const useItemListPaginatedLoader = ({
|
||||
);
|
||||
|
||||
const { data } = useQuery({
|
||||
gcTime: 1000 * 15,
|
||||
gcTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15,
|
||||
placeholderData: { items: getInitialData(itemsPerPage) },
|
||||
queryFn: async ({ signal }) => {
|
||||
const result = await listQueryFn({
|
||||
@@ -97,7 +99,7 @@ export const useItemListPaginatedLoader = ({
|
||||
return result;
|
||||
},
|
||||
queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams),
|
||||
staleTime: 1000 * 15,
|
||||
staleTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15,
|
||||
});
|
||||
|
||||
const refreshMutation = useMutation({
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { Album, AlbumArtist, Artist, LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
type PlayableArtistItem = AlbumArtist | Artist;
|
||||
|
||||
interface PlayerQueueByDataActions {
|
||||
addToQueueByData: (data: Song[], type: Play, playSongId?: string) => void;
|
||||
}
|
||||
|
||||
interface PlayerQueueByFetchActions {
|
||||
addToQueueByFetch: (
|
||||
serverId: string,
|
||||
ids: string[],
|
||||
itemType: LibraryItem,
|
||||
playType: Play,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const playSongFromItemListControl = ({
|
||||
index,
|
||||
internalState,
|
||||
item,
|
||||
meta,
|
||||
player,
|
||||
}: {
|
||||
index?: number;
|
||||
internalState?: ItemListStateActions;
|
||||
item: Song;
|
||||
meta?: Record<string, unknown>;
|
||||
player: PlayerQueueByDataActions;
|
||||
}) => {
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const singleSongOnly = meta?.singleSongOnly === true;
|
||||
|
||||
if (singleSongOnly) {
|
||||
player.addToQueueByData([item], playType, item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined && items) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
};
|
||||
|
||||
export const playAlbumFromItemListControl = ({
|
||||
album,
|
||||
meta,
|
||||
player,
|
||||
}: {
|
||||
album: Album;
|
||||
meta?: Record<string, unknown>;
|
||||
player: PlayerQueueByFetchActions;
|
||||
}) => {
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
player.addToQueueByFetch(album._serverId, [album.id], LibraryItem.ALBUM, playType);
|
||||
};
|
||||
|
||||
export const playArtistFromItemListControl = ({
|
||||
artist,
|
||||
itemType,
|
||||
meta,
|
||||
player,
|
||||
}: {
|
||||
artist: PlayableArtistItem;
|
||||
itemType: LibraryItem.ALBUM_ARTIST | LibraryItem.ARTIST;
|
||||
meta?: Record<string, unknown>;
|
||||
player: PlayerQueueByFetchActions;
|
||||
}) => {
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
player.addToQueueByFetch(artist._serverId, [artist.id], itemType, playType);
|
||||
};
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
ItemListStateItemWithRequiredProperties,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||
import { useHotkeySettings, usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
@@ -16,11 +16,13 @@ export const useListHotkeys = ({
|
||||
focused,
|
||||
internalState,
|
||||
itemType,
|
||||
onShowPlayingSong,
|
||||
}: {
|
||||
controls: ItemControls;
|
||||
focused: boolean;
|
||||
internalState: ItemListStateActions;
|
||||
itemType: LibraryItem;
|
||||
onShowPlayingSong?: () => void;
|
||||
}) => {
|
||||
const { bindings } = useHotkeySettings();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
@@ -119,5 +121,11 @@ export const useListHotkeys = ({
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
bindings.listShowPlayingSong.hotkey,
|
||||
() => {
|
||||
onShowPlayingSong?.();
|
||||
},
|
||||
],
|
||||
]);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user